From 53fb2000e83a264d060201133cbcdb2955dfc364 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:32:20 -0600 Subject: [PATCH 01/12] Add generic ListView with typed Value, SelectedItem, and Index properties - ListView extends ListView and implements IValue - Value (T?) is the primary property returning the selected object - SelectedItem (new T?) is a convenience alias for Value, mirroring how ListView.SelectedItem aliases ListView.Value - Index (int?) provides read/write access to the underlying int? index - SetSource(ObservableCollection?) wires the typed source - Event bridge translates base int? ValueChanging/ValueChanged events to typed T? events; cancellation propagates back correctly - 22 unit tests in UnitTestsParallelizable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Terminal.Gui/Views/ListView/ListViewT.cs | 206 +++++++++++++ .../Views/ListViewTTests.cs | 286 ++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 Terminal.Gui/Views/ListView/ListViewT.cs create mode 100644 Tests/UnitTestsParallelizable/Views/ListViewTTests.cs diff --git a/Terminal.Gui/Views/ListView/ListViewT.cs b/Terminal.Gui/Views/ListView/ListViewT.cs new file mode 100644 index 0000000000..dfcb5ee99c --- /dev/null +++ b/Terminal.Gui/Views/ListView/ListViewT.cs @@ -0,0 +1,206 @@ +using System.Collections.ObjectModel; + +namespace Terminal.Gui.Views; + +/// +/// Provides a scrollable list of data where each item can be activated to perform an action, +/// with a strongly-typed property that returns the selected object of type +/// from the underlying . +/// +/// The type of items in the collection. +/// +/// +/// extends by implementing +/// . The property returns the currently selected +/// object of type rather than the selected index. +/// +/// +/// All functionality (rendering, marking, keyboard navigation, +/// key and mouse bindings) is inherited unchanged. Use +/// to provide the typed source collection. +/// +/// +/// The base (index-based, with +/// T = int?) remains accessible by casting to or +/// IValue<int?>. +/// +/// +public class ListView : ListView, IValue +{ + private ObservableCollection? _typedSource; + + /// + /// Initializes a new instance of . + /// + public ListView () + { + base.ValueChanging += TranslateValueChanging; + base.ValueChanged += TranslateValueChanged; + } + + /// + /// Sets the source collection and updates the display. + /// + /// + /// The to display, + /// or to clear the list. + /// + public void SetSource (ObservableCollection? source) + { + _typedSource = source; + base.SetSource (source); + } + + #region IValue Implementation + + /// + /// Gets or sets the currently selected item as a object. + /// + /// + /// The selected item, or if no item is selected or the source is not set. + /// + /// + /// + /// The getter retrieves the object at the selected index from the typed source collection. + /// + /// + /// The setter locates the object in the collection and updates + /// to the corresponding index. If the object is not + /// found in the collection, the selection is unchanged. + /// + /// + public new T? Value + { + get => GetObjectAt (base.SelectedItem); + set + { + if (value is null) + { + base.SelectedItem = null; + + return; + } + + if (_typedSource is null) + { + return; + } + + int index = _typedSource.IndexOf (value); + + if (index < 0) + { + return; + } + + base.SelectedItem = index; + } + } + + /// + object? IValue.GetValue () => Value; + + /// + /// Gets or sets the currently selected object. + /// This is a convenience property that is an alias for . + /// + /// + /// The selected object of type , + /// or if no item is selected. + /// + public new T? SelectedItem { get => Value; set => Value = value; } + + /// + /// Gets or sets the zero-based index of the currently selected item. + /// + /// + /// The index of the selected item, or if no item is selected. + /// + /// + /// Use this property to get or set the selection by index directly. + /// To get or set the selection by object, use or . + /// + public int? Index { get => base.Value; set => base.Value = value; } + + /// + /// Called when is about to change. + /// + /// The event arguments containing the current and proposed typed values. + /// to cancel the change; otherwise . + protected virtual bool OnValueChanging (ValueChangingEventArgs args) => false; + + /// + /// Raised when is about to change. + /// Set to to cancel the change. + /// + public new event EventHandler>? ValueChanging; + + /// + /// Called when has changed. + /// + /// The event arguments containing the old and new typed values. + protected virtual void OnValueChanged (ValueChangedEventArgs args) { } + + /// + /// Raised when has changed. + /// + public new event EventHandler>? ValueChanged; + + /// + public new event EventHandler>? ValueChangedUntyped; + + #endregion IValue Implementation + + /// + protected override void Dispose (bool disposing) + { + if (disposing) + { + base.ValueChanging -= TranslateValueChanging; + base.ValueChanged -= TranslateValueChanged; + } + + base.Dispose (disposing); + } + + private T? GetObjectAt (int? index) + { + if (index is null || _typedSource is null || index < 0 || index >= _typedSource.Count) + { + return default; + } + + return _typedSource [index.Value]; + } + + private void TranslateValueChanging (object? sender, ValueChangingEventArgs intArgs) + { + T? oldObj = GetObjectAt (intArgs.CurrentValue); + T? newObj = GetObjectAt (intArgs.NewValue); + ValueChangingEventArgs tArgs = new (oldObj, newObj); + + if (OnValueChanging (tArgs) || tArgs.Handled) + { + intArgs.Handled = true; + + return; + } + + ValueChanging?.Invoke (this, tArgs); + + if (tArgs.Handled) + { + intArgs.Handled = true; + } + } + + private void TranslateValueChanged (object? sender, ValueChangedEventArgs intArgs) + { + T? oldObj = GetObjectAt (intArgs.OldValue); + T? newObj = GetObjectAt (intArgs.NewValue); + ValueChangedEventArgs tArgs = new (oldObj, newObj); + OnValueChanged (tArgs); + ValueChanged?.Invoke (this, tArgs); + ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs (oldObj, newObj)); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs new file mode 100644 index 0000000000..7d31eae0e4 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs @@ -0,0 +1,286 @@ +using System.Collections.ObjectModel; + +// Copilot + +namespace ViewsTests; + +public class ListViewTTests +{ + [Fact] + public void Value_ReturnsSelectedObject () + { + ObservableCollection source = ["one", "two", "three"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 1; + + Assert.Equal ("two", listView.Value); + } + + [Fact] + public void Value_IsNull_WhenNoSelection () + { + ObservableCollection source = ["one", "two"]; + ListView listView = new (); + listView.SetSource (source); + + Assert.Null (listView.Value); + } + + [Fact] + public void Value_IsNull_WhenSourceIsNull () + { + ListView listView = new (); + + Assert.Null (listView.Value); + } + + [Fact] + public void Value_Setter_SelectsCorrectIndex () + { + ObservableCollection source = ["alpha", "beta", "gamma"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + listView.Value = "beta"; + + Assert.Equal (1, listView.Index); + Assert.Equal ("beta", listView.Value); + } + + [Fact] + public void Value_Setter_Null_ClearsSelection () + { + ObservableCollection source = ["alpha", "beta"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + listView.Value = null; + + Assert.Null (listView.Index); + Assert.Null (listView.Value); + } + + [Fact] + public void Value_Setter_DoesNothing_WhenObjectNotInCollection () + { + ObservableCollection source = ["one", "two"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + listView.Value = "three"; + + Assert.Equal (0, listView.Index); + Assert.Equal ("one", listView.Value); + } + + [Fact] + public void Value_Setter_DoesNothing_WhenSourceIsNull () + { + ListView listView = new (); + + // Should not throw + listView.Value = "anything"; + + Assert.Null (listView.Index); + } + + [Fact] + public void ValueChanged_FiresWithTypedObject () + { + ObservableCollection source = ["a", "b", "c"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + ValueChangedEventArgs? receivedArgs = null; + listView.ValueChanged += (_, args) => receivedArgs = args; + + listView.Index = 2; + + Assert.NotNull (receivedArgs); + Assert.Equal ("a", receivedArgs!.OldValue); + Assert.Equal ("c", receivedArgs.NewValue); + } + + [Fact] + public void ValueChanging_FiresWithTypedObject () + { + ObservableCollection source = ["x", "y", "z"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + ValueChangingEventArgs? receivedArgs = null; + listView.ValueChanging += (_, args) => receivedArgs = args; + + listView.Index = 1; + + Assert.NotNull (receivedArgs); + Assert.Equal ("x", receivedArgs!.CurrentValue); + Assert.Equal ("y", receivedArgs.NewValue); + } + + [Fact] + public void ValueChanging_CanCancel () + { + ObservableCollection source = ["p", "q"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + listView.ValueChanging += (_, args) => args.Handled = true; + + listView.Index = 1; + + Assert.Equal (0, listView.Index); + Assert.Equal ("p", listView.Value); + } + + [Fact] + public void ValueChangedUntyped_FiresWithObjectNotIndex () + { + ObservableCollection source = ["first", "second"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + ValueChangedEventArgs? receivedArgs = null; + listView.ValueChangedUntyped += (_, args) => receivedArgs = args; + + listView.Index = 1; + + Assert.NotNull (receivedArgs); + Assert.Equal ("first", receivedArgs!.OldValue); + Assert.Equal ("second", receivedArgs.NewValue); + } + + [Fact] + public void GetValue_ReturnsTypedObject () + { + ObservableCollection source = ["item0", "item1"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 1; + + object? result = ((IValue)listView).GetValue (); + + Assert.Equal ("item1", result); + } + + [Fact] + public void SetSource_Null_DoesNotThrow () + { + ListView listView = new (); + listView.SetSource (["a", "b"]); + + // Should not throw + listView.SetSource (null); + } + + [Fact] + public void SetSource_Null_ValueIsNull () + { + ListView listView = new (); + listView.SetSource (["a", "b"]); + listView.SetSource (null); + + Assert.Null (listView.Value); + } + + [Fact] + public void Index_ReturnsSelectedItemIndex () + { + ObservableCollection source = ["one", "two", "three"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 2; + + Assert.Equal (2, listView.Index); + } + + [Fact] + public void Index_IsNull_WhenNoSelection () + { + ObservableCollection source = ["one", "two"]; + ListView listView = new (); + listView.SetSource (source); + + Assert.Null (listView.Index); + } + + [Fact] + public void Index_UpdatesWhenValueSetterChangesSelection () + { + ObservableCollection source = ["a", "b", "c"]; + ListView listView = new (); + listView.SetSource (source); + + listView.Value = "c"; + + Assert.Equal (2, listView.Index); + } + + [Fact] + public void Index_Setter_SelectsCorrectItem () + { + ObservableCollection source = ["one", "two", "three"]; + ListView listView = new (); + listView.SetSource (source); + + listView.Index = 2; + + Assert.Equal (2, listView.Index); + Assert.Equal ("three", listView.Value); + } + + [Fact] + public void Value_UsesObjectEquality_ForValueSetter () + { + ObservableCollection source = ["cat", "dog", "bird"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 0; + + listView.Value = "bird"; + + Assert.Equal (2, listView.Index); + } + + [Fact] + public void SelectedItem_ReturnsSelectedObject () + { + ObservableCollection source = ["one", "two", "three"]; + ListView listView = new (); + listView.SetSource (source); + listView.Index = 1; + + Assert.Equal ("two", listView.SelectedItem); + } + + [Fact] + public void SelectedItem_IsNull_WhenNoSelection () + { + ObservableCollection source = ["one", "two"]; + ListView listView = new (); + listView.SetSource (source); + + Assert.Null (listView.SelectedItem); + } + + [Fact] + public void SelectedItem_Setter_SelectsCorrectItem () + { + ObservableCollection source = ["alpha", "beta", "gamma"]; + ListView listView = new (); + listView.SetSource (source); + + listView.SelectedItem = "beta"; + + Assert.Equal (1, listView.Index); + Assert.Equal ("beta", listView.Value); + } +} From 05b70fb0a06b67632d0eba75a1347a580fd5d374 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:38:21 -0600 Subject: [PATCH 02/12] Add GenericListView UICatalog scenario demonstrating ListView Shows: - ListView.Value returning typed Country object (not int index) - SelectedItem as a convenience alias for Value - Index property for int? index access - Typed ValueChanging/ValueChanged events - Cancellation via ValueChanging.Handled Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UICatalog/Scenarios/GenericListView.cs | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 Examples/UICatalog/Scenarios/GenericListView.cs diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs new file mode 100644 index 0000000000..2f5f7a260f --- /dev/null +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -0,0 +1,183 @@ +#nullable enable +using System.Collections.ObjectModel; +using System.Text; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("Generic ListView", "Demonstrates ListView with typed Value, SelectedItem, and Index")] +[ScenarioCategory ("Controls")] +[ScenarioCategory ("ListView")] +public class GenericListView : Scenario +{ + private ListView? _listView; + private ObservableCollection _eventList = []; + private ListView? _eventListView; + private Label? _nameLabel; + private Label? _capitalLabel; + private Label? _populationLabel; + private Label? _indexLabel; + private CheckBox? _cancelNextCb; + private bool _cancelNext; + + /// + public override void Main () + { + ConfigurationManager.Enable (ConfigLocations.All); + using IApplication app = Application.Create (); + app.Init (); + + using Window appWindow = new () { Title = GetQuitKeyAndName () }; + + ObservableCollection countries = + [ + new ("Australia", "Canberra", 26_000_000), + new ("Brazil", "Brasília", 215_000_000), + new ("Canada", "Ottawa", 38_000_000), + new ("Denmark", "Copenhagen", 5_900_000), + new ("Egypt", "Cairo", 104_000_000), + new ("France", "Paris", 68_000_000), + new ("Germany", "Berlin", 84_000_000), + new ("Hungary", "Budapest", 9_700_000), + new ("India", "New Delhi", 1_428_000_000), + new ("Japan", "Tokyo", 124_000_000) + ]; + + // -- Cancel checkbox -------------------------------------------------- + _cancelNextCb = new CheckBox + { + X = 0, + Y = 0, + Text = "C_ancel next selection change" + }; + _cancelNextCb.ValueChanging += (_, args) => _cancelNext = args.NewValue == CheckState.Checked; + appWindow.Add (_cancelNextCb); + + // -- ListView ------------------------------------------------ + _listView = new ListView + { + Title = "_Countries", + X = 0, + Y = Pos.Bottom (_cancelNextCb) + 1, + Width = 22, + Height = Dim.Fill (4), + BorderStyle = LineStyle.Single + }; + _listView.SetSource (countries); + appWindow.Add (_listView); + + // -- Detail panel ----------------------------------------------------- + FrameView detailPanel = new () + { + Title = "_Selected", + X = Pos.Right (_listView) + 1, + Y = Pos.Top (_listView), + Width = Dim.Fill (), + Height = _listView.Height + }; + appWindow.Add (detailPanel); + + Label nameTitleLbl = new () { X = 1, Y = 1, Text = "Name: " }; + detailPanel.Add (nameTitleLbl); + _nameLabel = new Label { X = Pos.Right (nameTitleLbl), Y = 1, Width = Dim.Fill (1), Text = "(none)" }; + detailPanel.Add (_nameLabel); + + Label capitalTitleLbl = new () { X = 1, Y = 2, Text = "Capital: " }; + detailPanel.Add (capitalTitleLbl); + _capitalLabel = new Label { X = Pos.Right (capitalTitleLbl), Y = 2, Width = Dim.Fill (1), Text = "" }; + detailPanel.Add (_capitalLabel); + + Label populationTitleLbl = new () { X = 1, Y = 3, Text = "Population:" }; + detailPanel.Add (populationTitleLbl); + _populationLabel = new Label { X = Pos.Right (populationTitleLbl), Y = 3, Width = Dim.Fill (1), Text = "" }; + detailPanel.Add (_populationLabel); + + Label indexTitleLbl = new () { X = 1, Y = 5, Text = "Index: " }; + detailPanel.Add (indexTitleLbl); + _indexLabel = new Label { X = Pos.Right (indexTitleLbl), Y = 5, Width = Dim.Fill (1), Text = "" }; + detailPanel.Add (_indexLabel); + + // -- Event log -------------------------------------------------------- + _eventList = []; + _eventListView = new ListView + { + Title = "_Events", + X = 0, + Y = Pos.Bottom (_listView) + 1, + Width = Dim.Fill (), + Height = Dim.Fill (), + Source = new ListWrapper (_eventList), + BorderStyle = LineStyle.Single + }; + appWindow.Add (_eventListView); + + // -- Wire events ------------------------------------------------------ + _listView.ValueChanging += OnValueChanging; + _listView.ValueChanged += OnValueChanged; + + app.Run (appWindow); + } + + private void OnValueChanging (object? sender, ValueChangingEventArgs args) + { + if (_cancelNext) + { + args.Handled = true; + _cancelNext = false; + + if (_cancelNextCb is not null) + { + _cancelNextCb.Value = CheckState.UnChecked; + } + + LogEvent ($"ValueChanging CANCELLED: {FormatCountry (args.CurrentValue)} -> {FormatCountry (args.NewValue)}"); + + return; + } + + LogEvent ($"ValueChanging: {FormatCountry (args.CurrentValue)} -> {FormatCountry (args.NewValue)}"); + } + + private void OnValueChanged (object? sender, ValueChangedEventArgs args) + { + UpdateDetail (args.NewValue); + LogEvent ($"ValueChanged: {FormatCountry (args.OldValue)} -> {FormatCountry (args.NewValue)}"); + } + + private void UpdateDetail (Country? country) + { + if (_nameLabel is null) + { + return; + } + + if (country is null) + { + _nameLabel.Text = "(none)"; + _capitalLabel!.Text = ""; + _populationLabel!.Text = ""; + _indexLabel!.Text = ""; + + return; + } + + _nameLabel.Text = country.Name; + _capitalLabel!.Text = country.Capital; + _populationLabel!.Text = $"{country.Population:N0}"; + _indexLabel!.Text = _listView?.Index?.ToString () ?? ""; + } + + private void LogEvent (string message) + { + _eventList.Add (message); + + if (_eventListView is not null) + { + _eventListView.MoveEnd (); + } + } + + private static string FormatCountry (Country? c) => c is null ? "null" : c.Name; +} + +/// A simple record used to demonstrate . +internal record Country (string Name, string Capital, int Population); From 70b5b93371dd977c231f6dac4c67ef0d1c8b9342 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:00:04 -0600 Subject: [PATCH 03/12] Add AspectGetter delegate to ListWrapper and ListView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ListWrapper.AspectGetter (Func?) lets callers provide a custom display-text converter instead of relying on ToString(). Setting the property recalculates MaxItemLength immediately. ListView.AspectGetter exposes the same delegate and wires it to the underlying ListWrapper automatically — works whether set before or after SetSource(). GenericListView UICatalog scenario updated to use: AspectGetter = c => c.Name Tests added: - ListWrapperTests.cs (4 new tests for ListWrapper directly) - ListViewTTests.cs (2 new integration tests via ListView) Total: 28 passing tests across both files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UICatalog/Scenarios/GenericListView.cs | 3 +- Terminal.Gui/Views/ListView/ListViewT.cs | 32 ++++++++++ Terminal.Gui/Views/ListView/ListWrapper.cs | 56 +++++++++++++++--- .../Views/ListViewTTests.cs | 26 +++++++++ .../Views/ListWrapperTests.cs | 58 +++++++++++++++++++ 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index 2f5f7a260f..2cdb67e6d6 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -60,7 +60,8 @@ public override void Main () Y = Pos.Bottom (_cancelNextCb) + 1, Width = 22, Height = Dim.Fill (4), - BorderStyle = LineStyle.Single + BorderStyle = LineStyle.Single, + AspectGetter = c => c.Name }; _listView.SetSource (countries); appWindow.Add (_listView); diff --git a/Terminal.Gui/Views/ListView/ListViewT.cs b/Terminal.Gui/Views/ListView/ListViewT.cs index dfcb5ee99c..6c269c5ba1 100644 --- a/Terminal.Gui/Views/ListView/ListViewT.cs +++ b/Terminal.Gui/Views/ListView/ListViewT.cs @@ -28,6 +28,7 @@ namespace Terminal.Gui.Views; public class ListView : ListView, IValue { private ObservableCollection? _typedSource; + private Func? _aspectGetter; /// /// Initializes a new instance of . @@ -38,6 +39,32 @@ public ListView () base.ValueChanged += TranslateValueChanged; } + /// + /// Gets or sets a delegate that converts an item of type to its display string. + /// When (the default), items are converted using . + /// + /// + /// A that accepts an item and returns the string to display, + /// or to use . + /// + /// + /// Setting this property updates the underlying immediately if the source is + /// already set. It is also applied automatically whenever is called. + /// + public Func? AspectGetter + { + get => _aspectGetter; + set + { + _aspectGetter = value; + + if (Source is ListWrapper wrapper) + { + wrapper.AspectGetter = value; + } + } + } + /// /// Sets the source collection and updates the display. /// @@ -49,6 +76,11 @@ public void SetSource (ObservableCollection? source) { _typedSource = source; base.SetSource (source); + + if (_aspectGetter is not null && Source is ListWrapper wrapper) + { + wrapper.AspectGetter = _aspectGetter; + } } #region IValue Implementation diff --git a/Terminal.Gui/Views/ListView/ListWrapper.cs b/Terminal.Gui/Views/ListView/ListWrapper.cs index 5f86d1fb4f..bf8b549916 100644 --- a/Terminal.Gui/Views/ListView/ListWrapper.cs +++ b/Terminal.Gui/Views/ListView/ListWrapper.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.Views; /// /// Provides a default implementation of that renders items -/// using . +/// using , or a custom delegate when provided. /// public class ListWrapper : IListDataSource { @@ -31,6 +31,28 @@ public ListWrapper (ObservableCollection? source) private readonly ObservableCollection? _source; private int _count; private BitArray? _marks; + private Func? _aspectGetter; + + /// + /// Gets or sets a delegate that converts an item of type to its display string. + /// When (the default), items are converted using . + /// + /// + /// A that accepts an item and returns the string to display, + /// or to use . + /// + /// + /// Setting this property immediately recalculates . + /// + public Func? AspectGetter + { + get => _aspectGetter; + set + { + _aspectGetter = value; + MaxItemLength = GetMaxLengthItem (); + } + } /// public event NotifyCollectionChangedEventHandler? CollectionChanged; @@ -66,15 +88,22 @@ public void Render (ListView container, bool marked, int item, int col, int line return; } - object? t = _source [item]; + T typedItem = _source [item]; + + if (_aspectGetter is not null) + { + RenderString (container, _aspectGetter (typedItem), width, viewportX); + + return; + } - switch (t) + switch ((object?)typedItem) { case null: RenderString (container, "", width); break; case string s: RenderString (container, s, width, viewportX); break; - default: RenderString (container, t.ToString ()!, width, viewportX); break; + default: RenderString (container, typedItem!.ToString ()!, width, viewportX); break; } } @@ -191,14 +220,25 @@ private int GetMaxLengthItem () for (var i = 0; i < _source!.Count; i++) { - object? t = _source [i]; + T typedItem = _source [i]; + + int l; - if (t is null) + if (_aspectGetter is not null) { - continue; + l = _aspectGetter (typedItem).GetColumns (); } + else + { + object? t = typedItem; - int l = t is string u ? u.GetColumns () : t.ToString ()!.Length; + if (t is null) + { + continue; + } + + l = t is string u ? u.GetColumns () : t.ToString ()!.Length; + } if (l > maxLength) { diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs index 7d31eae0e4..baa96256c9 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs @@ -283,4 +283,30 @@ public void SelectedItem_Setter_SelectsCorrectItem () Assert.Equal (1, listView.Index); Assert.Equal ("beta", listView.Value); } + + [Fact] + public void AspectGetter_SetBeforeSetSource_AppliedToWrapper () + { + ObservableCollection source = ["a", "bb", "ccc"]; + ListView listView = new (); + listView.AspectGetter = s => $"[{s}]"; // "[ccc]" = 5 + + listView.SetSource (source); + + Assert.Equal (5, listView.Source?.MaxItemLength); + } + + [Fact] + public void AspectGetter_SetAfterSetSource_UpdatesWrapper () + { + ObservableCollection source = ["a", "bb", "ccc"]; + ListView listView = new (); + listView.SetSource (source); + + int before = listView.Source?.MaxItemLength ?? 0; // "ccc" = 3 + listView.AspectGetter = s => $"({s})"; // "(ccc)" = 5 + + Assert.Equal (3, before); + Assert.Equal (5, listView.Source?.MaxItemLength); + } } diff --git a/Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs b/Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs new file mode 100644 index 0000000000..7a540119f0 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs @@ -0,0 +1,58 @@ +using System.Collections.ObjectModel; + +// Copilot + +namespace ViewsTests; + +public class ListWrapperTests +{ + [Fact] + public void AspectGetter_Null_UsesToString () + { + ObservableCollection source = [1, 22, 333]; + ListWrapper wrapper = new (source); + + Assert.Equal (3, wrapper.MaxItemLength); // "333".Length + } + + [Fact] + public void AspectGetter_WhenSet_UsesDelegate () + { + ObservableCollection source = [1, 22, 333]; + ListWrapper wrapper = new (source) + { + AspectGetter = n => $"item-{n}" + }; + + // "item-333" = 8 chars + Assert.Equal (8, wrapper.MaxItemLength); + } + + [Fact] + public void AspectGetter_WhenSet_UpdatesMaxItemLength () + { + ObservableCollection source = [1, 22, 333]; + ListWrapper wrapper = new (source); + + int before = wrapper.MaxItemLength; // "333" = 3 + + wrapper.AspectGetter = n => n.ToString ("D6"); // "000333" = 6 + + Assert.Equal (3, before); + Assert.Equal (6, wrapper.MaxItemLength); + } + + [Fact] + public void AspectGetter_ClearedToNull_RecalculatesWithToString () + { + ObservableCollection source = [1, 22, 333]; + ListWrapper wrapper = new (source) + { + AspectGetter = n => n.ToString ("D6") // "000333" = 6 + }; + + wrapper.AspectGetter = null; + + Assert.Equal (3, wrapper.MaxItemLength); // back to "333" = 3 + } +} From c5188e21bcd788d07f3a7b1ba845335a8cde34a5 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:12:35 -0600 Subject: [PATCH 04/12] Improve IValue.GetValue() XML doc in ListView Replaces inheritdoc with explicit documentation clarifying that GetValue() returns the typed T? object (not the int? index that base ListView.GetValue() returns). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Terminal.Gui/Views/ListView/ListViewT.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/ListView/ListViewT.cs b/Terminal.Gui/Views/ListView/ListViewT.cs index 6c269c5ba1..55a86fb954 100644 --- a/Terminal.Gui/Views/ListView/ListViewT.cs +++ b/Terminal.Gui/Views/ListView/ListViewT.cs @@ -129,7 +129,19 @@ public void SetSource (ObservableCollection? source) } } - /// + /// + /// Gets the currently selected item as a boxed object. + /// + /// + /// The selected item of type , boxed as , + /// or if no item is selected. + /// + /// + /// This explicit implementation overrides the base 's behavior, + /// which returns the selected index as . + /// Here, the returned value is the selected object from the typed source collection, + /// consistent with . + /// object? IValue.GetValue () => Value; /// From 5fadf8d4b5bc771f808b94581cc07f2e81400be7 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:08:50 -0600 Subject: [PATCH 05/12] Update Examples/UICatalog/Scenarios/GenericListView.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/UICatalog/Scenarios/GenericListView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index 2cdb67e6d6..da2e927de9 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -79,7 +79,7 @@ public override void Main () Label nameTitleLbl = new () { X = 1, Y = 1, Text = "Name: " }; detailPanel.Add (nameTitleLbl); - _nameLabel = new Label { X = Pos.Right (nameTitleLbl), Y = 1, Width = Dim.Fill (1), Text = "(none)" }; + _nameLabel = new () { X = Pos.Right (nameTitleLbl), Y = 1, Width = Dim.Fill (1), Text = "(none)" }; detailPanel.Add (_nameLabel); Label capitalTitleLbl = new () { X = 1, Y = 2, Text = "Capital: " }; From 95a68ec75bcb7a7042287ecff0c4a1585ccb2d42 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:09:18 -0600 Subject: [PATCH 06/12] Update Examples/UICatalog/Scenarios/GenericListView.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/UICatalog/Scenarios/GenericListView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index da2e927de9..3714535dff 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -84,7 +84,7 @@ public override void Main () Label capitalTitleLbl = new () { X = 1, Y = 2, Text = "Capital: " }; detailPanel.Add (capitalTitleLbl); - _capitalLabel = new Label { X = Pos.Right (capitalTitleLbl), Y = 2, Width = Dim.Fill (1), Text = "" }; + _capitalLabel = new () { X = Pos.Right (capitalTitleLbl), Y = 2, Width = Dim.Fill (1), Text = "" }; detailPanel.Add (_capitalLabel); Label populationTitleLbl = new () { X = 1, Y = 3, Text = "Population:" }; From ecb82b059ba168f77c2ec1a5445311e35ecd0c7e Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:09:39 -0600 Subject: [PATCH 07/12] Update Examples/UICatalog/Scenarios/GenericListView.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/UICatalog/Scenarios/GenericListView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index 3714535dff..994d9b495f 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -89,7 +89,7 @@ public override void Main () Label populationTitleLbl = new () { X = 1, Y = 3, Text = "Population:" }; detailPanel.Add (populationTitleLbl); - _populationLabel = new Label { X = Pos.Right (populationTitleLbl), Y = 3, Width = Dim.Fill (1), Text = "" }; + _populationLabel = new () { X = Pos.Right (populationTitleLbl), Y = 3, Width = Dim.Fill (1), Text = "" }; detailPanel.Add (_populationLabel); Label indexTitleLbl = new () { X = 1, Y = 5, Text = "Index: " }; From 5cc6b8661b6b2282df83a3382ca6b992bfecf2b3 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:09:48 -0600 Subject: [PATCH 08/12] Update Examples/UICatalog/Scenarios/GenericListView.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/UICatalog/Scenarios/GenericListView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index 994d9b495f..699aaf23f1 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -94,7 +94,7 @@ public override void Main () Label indexTitleLbl = new () { X = 1, Y = 5, Text = "Index: " }; detailPanel.Add (indexTitleLbl); - _indexLabel = new Label { X = Pos.Right (indexTitleLbl), Y = 5, Width = Dim.Fill (1), Text = "" }; + _indexLabel = new () { X = Pos.Right (indexTitleLbl), Y = 5, Width = Dim.Fill (1), Text = "" }; detailPanel.Add (_indexLabel); // -- Event log -------------------------------------------------------- From 9fda182828a358a2e39add8514f81cd3799b4663 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:00:48 -0600 Subject: [PATCH 09/12] Address PR feedback: remove AspectGetter/ColorGetter from library - Remove AspectGetter and ColorGetter from ListWrapper and ListView - Delete IAspectGetter and IColorGetter interfaces (no longer needed) - Fix GetMaxLengthItem(): use .GetColumns() instead of .Length for correct Unicode width measurement; rename pattern variable u -> s - Update GenericListView scenario to demonstrate the same functionality using built-in mechanisms (per BDisp's suggestion): - Country.ToString() override for display text (replaces AspectGetter) - ListView.RowRender event for per-row coloring (replaces ColorGetter) - Remove AspectGetter/ColorGetter tests from ListViewTTests.cs - Remove ListWrapperTests.cs (all tests were AspectGetter/ColorGetter) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UICatalog/Scenarios/GenericListView.cs | 40 ++++++++++--- Terminal.Gui/Views/ListView/ListViewT.cs | 32 ---------- Terminal.Gui/Views/ListView/ListWrapper.cs | 60 ++++--------------- .../Views/ListViewTTests.cs | 26 -------- .../Views/ListWrapperTests.cs | 58 ------------------ 5 files changed, 43 insertions(+), 173 deletions(-) delete mode 100644 Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index 699aaf23f1..d8c0c45324 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -4,7 +4,7 @@ namespace UICatalog.Scenarios; -[ScenarioMetadata ("Generic ListView", "Demonstrates ListView with typed Value, SelectedItem, and Index")] +[ScenarioMetadata ("Generic ListView", "Demonstrates ListView with typed Value, SelectedItem, Index, and RowRender for custom row coloring")] [ScenarioCategory ("Controls")] [ScenarioCategory ("ListView")] public class GenericListView : Scenario @@ -59,13 +59,32 @@ public override void Main () X = 0, Y = Pos.Bottom (_cancelNextCb) + 1, Width = 22, - Height = Dim.Fill (4), - BorderStyle = LineStyle.Single, - AspectGetter = c => c.Name + Height = Dim.Fill (), + BorderStyle = LineStyle.Single }; _listView.SetSource (countries); appWindow.Add (_listView); + // Build highlight scheme by inheriting the resolved scheme and only + // overriding foreground colors so the background stays theme-consistent. + Scheme baseScheme = _listView.GetScheme (); + Scheme highlightScheme = new () + { + Normal = baseScheme.Normal with { Foreground = Color.BrightRed }, + Focus = baseScheme.Focus with { Foreground = Color.BrightRed, Style = TextStyle.Bold } + }; + + // Use RowRender to color rows with population > 100M (demonstrates + // how to achieve per-row coloring without a ColorGetter delegate). + _listView.RowRender += (_, args) => + { + if (args.Row < countries.Count && countries [args.Row].Population > 100_000_000) + { + bool isSelected = args.Row == _listView.Index; + args.RowAttribute = isSelected ? highlightScheme.Focus : highlightScheme.Normal; + } + }; + // -- Detail panel ----------------------------------------------------- FrameView detailPanel = new () { @@ -73,7 +92,7 @@ public override void Main () X = Pos.Right (_listView) + 1, Y = Pos.Top (_listView), Width = Dim.Fill (), - Height = _listView.Height + Height = 9 }; appWindow.Add (detailPanel); @@ -102,8 +121,8 @@ public override void Main () _eventListView = new ListView { Title = "_Events", - X = 0, - Y = Pos.Bottom (_listView) + 1, + X = Pos.Right (_listView) + 1, + Y = Pos.Bottom (detailPanel) + 1, Width = Dim.Fill (), Height = Dim.Fill (), Source = new ListWrapper (_eventList), @@ -181,4 +200,9 @@ private void LogEvent (string message) } /// A simple record used to demonstrate . -internal record Country (string Name, string Capital, int Population); +internal record Country (string Name, string Capital, int Population) +{ + // Overriding ToString() so ListView displays the country name + // without needing an AspectGetter delegate. + public override string ToString () => Name; +} diff --git a/Terminal.Gui/Views/ListView/ListViewT.cs b/Terminal.Gui/Views/ListView/ListViewT.cs index 55a86fb954..3b9bf32b4f 100644 --- a/Terminal.Gui/Views/ListView/ListViewT.cs +++ b/Terminal.Gui/Views/ListView/ListViewT.cs @@ -28,7 +28,6 @@ namespace Terminal.Gui.Views; public class ListView : ListView, IValue { private ObservableCollection? _typedSource; - private Func? _aspectGetter; /// /// Initializes a new instance of . @@ -39,32 +38,6 @@ public ListView () base.ValueChanged += TranslateValueChanged; } - /// - /// Gets or sets a delegate that converts an item of type to its display string. - /// When (the default), items are converted using . - /// - /// - /// A that accepts an item and returns the string to display, - /// or to use . - /// - /// - /// Setting this property updates the underlying immediately if the source is - /// already set. It is also applied automatically whenever is called. - /// - public Func? AspectGetter - { - get => _aspectGetter; - set - { - _aspectGetter = value; - - if (Source is ListWrapper wrapper) - { - wrapper.AspectGetter = value; - } - } - } - /// /// Sets the source collection and updates the display. /// @@ -76,11 +49,6 @@ public void SetSource (ObservableCollection? source) { _typedSource = source; base.SetSource (source); - - if (_aspectGetter is not null && Source is ListWrapper wrapper) - { - wrapper.AspectGetter = _aspectGetter; - } } #region IValue Implementation diff --git a/Terminal.Gui/Views/ListView/ListWrapper.cs b/Terminal.Gui/Views/ListView/ListWrapper.cs index bf8b549916..b2b4995cd2 100644 --- a/Terminal.Gui/Views/ListView/ListWrapper.cs +++ b/Terminal.Gui/Views/ListView/ListWrapper.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.Views; /// /// Provides a default implementation of that renders items -/// using , or a custom delegate when provided. +/// using . /// public class ListWrapper : IListDataSource { @@ -31,28 +31,6 @@ public ListWrapper (ObservableCollection? source) private readonly ObservableCollection? _source; private int _count; private BitArray? _marks; - private Func? _aspectGetter; - - /// - /// Gets or sets a delegate that converts an item of type to its display string. - /// When (the default), items are converted using . - /// - /// - /// A that accepts an item and returns the string to display, - /// or to use . - /// - /// - /// Setting this property immediately recalculates . - /// - public Func? AspectGetter - { - get => _aspectGetter; - set - { - _aspectGetter = value; - MaxItemLength = GetMaxLengthItem (); - } - } /// public event NotifyCollectionChangedEventHandler? CollectionChanged; @@ -90,21 +68,14 @@ public void Render (ListView container, bool marked, int item, int col, int line T typedItem = _source [item]; - if (_aspectGetter is not null) - { - RenderString (container, _aspectGetter (typedItem), width, viewportX); - - return; - } - - switch ((object?)typedItem) + string text = typedItem switch { - case null: RenderString (container, "", width); break; - - case string s: RenderString (container, s, width, viewportX); break; + null => "", + string s => s, + _ => typedItem.ToString ()! + }; - default: RenderString (container, typedItem!.ToString ()!, width, viewportX); break; - } + RenderString (container, text, width, viewportX); } /// @@ -222,23 +193,14 @@ private int GetMaxLengthItem () { T typedItem = _source [i]; - int l; + object? t = typedItem; - if (_aspectGetter is not null) + if (t is null) { - l = _aspectGetter (typedItem).GetColumns (); + continue; } - else - { - object? t = typedItem; - if (t is null) - { - continue; - } - - l = t is string u ? u.GetColumns () : t.ToString ()!.Length; - } + int l = t is string s ? s.GetColumns () : t.ToString ()!.GetColumns (); if (l > maxLength) { diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs index baa96256c9..7d31eae0e4 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTTests.cs @@ -283,30 +283,4 @@ public void SelectedItem_Setter_SelectsCorrectItem () Assert.Equal (1, listView.Index); Assert.Equal ("beta", listView.Value); } - - [Fact] - public void AspectGetter_SetBeforeSetSource_AppliedToWrapper () - { - ObservableCollection source = ["a", "bb", "ccc"]; - ListView listView = new (); - listView.AspectGetter = s => $"[{s}]"; // "[ccc]" = 5 - - listView.SetSource (source); - - Assert.Equal (5, listView.Source?.MaxItemLength); - } - - [Fact] - public void AspectGetter_SetAfterSetSource_UpdatesWrapper () - { - ObservableCollection source = ["a", "bb", "ccc"]; - ListView listView = new (); - listView.SetSource (source); - - int before = listView.Source?.MaxItemLength ?? 0; // "ccc" = 3 - listView.AspectGetter = s => $"({s})"; // "(ccc)" = 5 - - Assert.Equal (3, before); - Assert.Equal (5, listView.Source?.MaxItemLength); - } } diff --git a/Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs b/Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs deleted file mode 100644 index 7a540119f0..0000000000 --- a/Tests/UnitTestsParallelizable/Views/ListWrapperTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.ObjectModel; - -// Copilot - -namespace ViewsTests; - -public class ListWrapperTests -{ - [Fact] - public void AspectGetter_Null_UsesToString () - { - ObservableCollection source = [1, 22, 333]; - ListWrapper wrapper = new (source); - - Assert.Equal (3, wrapper.MaxItemLength); // "333".Length - } - - [Fact] - public void AspectGetter_WhenSet_UsesDelegate () - { - ObservableCollection source = [1, 22, 333]; - ListWrapper wrapper = new (source) - { - AspectGetter = n => $"item-{n}" - }; - - // "item-333" = 8 chars - Assert.Equal (8, wrapper.MaxItemLength); - } - - [Fact] - public void AspectGetter_WhenSet_UpdatesMaxItemLength () - { - ObservableCollection source = [1, 22, 333]; - ListWrapper wrapper = new (source); - - int before = wrapper.MaxItemLength; // "333" = 3 - - wrapper.AspectGetter = n => n.ToString ("D6"); // "000333" = 6 - - Assert.Equal (3, before); - Assert.Equal (6, wrapper.MaxItemLength); - } - - [Fact] - public void AspectGetter_ClearedToNull_RecalculatesWithToString () - { - ObservableCollection source = [1, 22, 333]; - ListWrapper wrapper = new (source) - { - AspectGetter = n => n.ToString ("D6") // "000333" = 6 - }; - - wrapper.AspectGetter = null; - - Assert.Equal (3, wrapper.MaxItemLength); // back to "333" = 3 - } -} From 38b14de9dfbd40c5910bc2401f3b882bef080c14 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:08:12 -0600 Subject: [PATCH 10/12] Revert cosmetic ListWrapper changes, keep GetColumns() fix Render() and GetMaxLengthItem() cosmetic refactors (object? vs T typedItem, switch statement vs switch expression) are reverted so ListWrapper is minimal-diff from upstream. The GetMaxLengthItem() bug fix is retained: use .GetColumns() instead of .Length for correct Unicode width measurement, and rename pattern variable u -> s (per PR review feedback). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Terminal.Gui/Views/ListView/ListWrapper.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/Views/ListView/ListWrapper.cs b/Terminal.Gui/Views/ListView/ListWrapper.cs index b2b4995cd2..e2fc837f4d 100644 --- a/Terminal.Gui/Views/ListView/ListWrapper.cs +++ b/Terminal.Gui/Views/ListView/ListWrapper.cs @@ -66,16 +66,16 @@ public void Render (ListView container, bool marked, int item, int col, int line return; } - T typedItem = _source [item]; + object? t = _source [item]; - string text = typedItem switch + switch (t) { - null => "", - string s => s, - _ => typedItem.ToString ()! - }; + case null: RenderString (container, "", width); break; - RenderString (container, text, width, viewportX); + case string s: RenderString (container, s, width, viewportX); break; + + default: RenderString (container, t.ToString ()!, width, viewportX); break; + } } /// @@ -191,9 +191,7 @@ private int GetMaxLengthItem () for (var i = 0; i < _source!.Count; i++) { - T typedItem = _source [i]; - - object? t = typedItem; + object? t = _source [i]; if (t is null) { From a4c3f9e28794061c81747cfffcc294d3fcb076ab Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:46:32 -0600 Subject: [PATCH 11/12] code formatting --- .../UICatalog/Scenarios/GenericListView.cs | 28 ++++++------------- Terminal.Gui/Views/ListView/ListViewT.cs | 18 ++++++------ 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/Examples/UICatalog/Scenarios/GenericListView.cs b/Examples/UICatalog/Scenarios/GenericListView.cs index d8c0c45324..4a394e8c4d 100644 --- a/Examples/UICatalog/Scenarios/GenericListView.cs +++ b/Examples/UICatalog/Scenarios/GenericListView.cs @@ -1,6 +1,5 @@ #nullable enable using System.Collections.ObjectModel; -using System.Text; namespace UICatalog.Scenarios; @@ -26,7 +25,8 @@ public override void Main () using IApplication app = Application.Create (); app.Init (); - using Window appWindow = new () { Title = GetQuitKeyAndName () }; + using Window appWindow = new (); + appWindow.Title = GetQuitKeyAndName (); ObservableCollection countries = [ @@ -42,7 +42,6 @@ public override void Main () new ("Japan", "Tokyo", 124_000_000) ]; - // -- Cancel checkbox -------------------------------------------------- _cancelNextCb = new CheckBox { X = 0, @@ -52,7 +51,6 @@ public override void Main () _cancelNextCb.ValueChanging += (_, args) => _cancelNext = args.NewValue == CheckState.Checked; appWindow.Add (_cancelNextCb); - // -- ListView ------------------------------------------------ _listView = new ListView { Title = "_Countries", @@ -75,7 +73,7 @@ public override void Main () }; // Use RowRender to color rows with population > 100M (demonstrates - // how to achieve per-row coloring without a ColorGetter delegate). + // how to achieve per-row coloring). _listView.RowRender += (_, args) => { if (args.Row < countries.Count && countries [args.Row].Population > 100_000_000) @@ -85,7 +83,6 @@ public override void Main () } }; - // -- Detail panel ----------------------------------------------------- FrameView detailPanel = new () { Title = "_Selected", @@ -116,7 +113,6 @@ public override void Main () _indexLabel = new () { X = Pos.Right (indexTitleLbl), Y = 5, Width = Dim.Fill (1), Text = "" }; detailPanel.Add (_indexLabel); - // -- Event log -------------------------------------------------------- _eventList = []; _eventListView = new ListView { @@ -130,7 +126,6 @@ public override void Main () }; appWindow.Add (_eventListView); - // -- Wire events ------------------------------------------------------ _listView.ValueChanging += OnValueChanging; _listView.ValueChanged += OnValueChanged; @@ -144,10 +139,7 @@ private void OnValueChanging (object? sender, ValueChangingEventArgs a args.Handled = true; _cancelNext = false; - if (_cancelNextCb is not null) - { - _cancelNextCb.Value = CheckState.UnChecked; - } + _cancelNextCb?.Value = CheckState.UnChecked; LogEvent ($"ValueChanging CANCELLED: {FormatCountry (args.CurrentValue)} -> {FormatCountry (args.NewValue)}"); @@ -190,19 +182,15 @@ private void LogEvent (string message) { _eventList.Add (message); - if (_eventListView is not null) - { - _eventListView.MoveEnd (); - } + _eventListView?.MoveEnd (); } - private static string FormatCountry (Country? c) => c is null ? "null" : c.Name; + private static string FormatCountry (Country? c) { return c is null ? "null" : c.Name; } } /// A simple record used to demonstrate . internal record Country (string Name, string Capital, int Population) { - // Overriding ToString() so ListView displays the country name - // without needing an AspectGetter delegate. - public override string ToString () => Name; + // Overriding ToString() so ListView only displays the country name. + public override string ToString () { return Name; } } diff --git a/Terminal.Gui/Views/ListView/ListViewT.cs b/Terminal.Gui/Views/ListView/ListViewT.cs index 3b9bf32b4f..53f5567ef5 100644 --- a/Terminal.Gui/Views/ListView/ListViewT.cs +++ b/Terminal.Gui/Views/ListView/ListViewT.cs @@ -5,22 +5,22 @@ namespace Terminal.Gui.Views; /// /// Provides a scrollable list of data where each item can be activated to perform an action, /// with a strongly-typed property that returns the selected object of type -/// from the underlying . +/// from the underlying . /// /// The type of items in the collection. /// /// -/// extends by implementing -/// . The property returns the currently selected +/// extends by implementing +/// . The property returns the currently selected /// object of type rather than the selected index. /// /// /// All functionality (rendering, marking, keyboard navigation, /// key and mouse bindings) is inherited unchanged. Use -/// to provide the typed source collection. +/// to provide the typed source collection. /// /// -/// The base (index-based, with +/// The base (index-based, with /// T = int?) remains accessible by casting to or /// IValue<int?>. /// @@ -30,7 +30,7 @@ public class ListView : ListView, IValue private ObservableCollection? _typedSource; /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// public ListView () { @@ -42,13 +42,13 @@ public ListView () /// Sets the source collection and updates the display. /// /// - /// The to display, + /// The to display, /// or to clear the list. /// public void SetSource (ObservableCollection? source) { _typedSource = source; - base.SetSource (source); + base.SetSource (source); } #region IValue Implementation @@ -179,7 +179,7 @@ protected override void Dispose (bool disposing) { if (index is null || _typedSource is null || index < 0 || index >= _typedSource.Count) { - return default; + return default (T?); } return _typedSource [index.Value]; From 5759f37ca0371bbaf5906a23a48f0be5665f25b4 Mon Sep 17 00:00:00 2001 From: YourRobotOverlord <47068588+YourRobotOverlord@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:57:48 -0600 Subject: [PATCH 12/12] Clarify ListView.Value setter behavior in XML docs Document that setting Value to an object not in the collection is a no-op (selection unchanged), and explain why this differs from the base ListView.SelectedItem setter which throws ArgumentException for out-of-range indexes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Terminal.Gui/Views/ListView/ListViewT.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Views/ListView/ListViewT.cs b/Terminal.Gui/Views/ListView/ListViewT.cs index 53f5567ef5..c9ddb252b4 100644 --- a/Terminal.Gui/Views/ListView/ListViewT.cs +++ b/Terminal.Gui/Views/ListView/ListViewT.cs @@ -65,8 +65,18 @@ public void SetSource (ObservableCollection? source) /// /// /// The setter locates the object in the collection and updates - /// to the corresponding index. If the object is not - /// found in the collection, the selection is unchanged. + /// to the corresponding index. + /// + /// + /// If is , the selection is cleared. + /// + /// + /// If the source collection has not been set, or if is not found + /// in the collection, the setter is a no-op and the selection remains unchanged. + /// This differs from the base setter, which throws + /// for an out-of-range index. Here, a value not present in + /// the collection is not considered an error — the caller may hold a stale reference or the + /// collection may have changed since the reference was obtained. /// /// public new T? Value