Sfoglia il codice sorgente

Fixes #2575 - TableView to use interface instead of System.Data.DataTable (#2576)

* WIP: Add ITableDataSource

* WIP: Refactor TableView

* WIP: Port CSVEditor

* WIP: Port TableEditor

* WIP: Port MultiColouredTable scenario

* Fix bug of adding duplicate column styles

* Update tests to use DataTableSource

* Tidy up

* Add EnumerableTableDataSource<T>

* Add test for EnumerableTableDataSource

* Add test for EnumerableTableDataSource

* Add code example to xmldoc

* Add ProcessTable scenario

* Rename ITableDataSource to ITableSource and update docs

* Rename EnumerableTableDataSource to EnumerableTableSource

* Fixed Frame != Bounds; changed UICat Scenarios list to use tableview!

* Fix scroll resetting in ProcessTable scenario

* Fix unit tests by setting Frame to same as Bounds

* Document why we have to measure our data for use with TableView

---------

Co-authored-by: Tig Kindel <[email protected]>
Thomas Nind 2 anni fa
parent
commit
038cf8aa45

+ 38 - 32
Terminal.Gui/Views/FileDialog.cs

@@ -107,7 +107,7 @@ namespace Terminal.Gui {
 		private MenuBar allowedTypeMenuBar;
 		private MenuBarItem allowedTypeMenu;
 		private MenuItem [] allowedTypeMenuItems;
-		private DataColumn filenameColumn;
+		private int filenameColumn;
 
 		/// <summary>
 		/// Event fired when user attempts to confirm a selection (or multi selection).
@@ -226,7 +226,7 @@ namespace Terminal.Gui {
 				if (this.tableView.SelectedRow <= 0) {
 					this.NavigateIf (k, Key.CursorUp, this.tbPath);
 				}
-				if (this.tableView.SelectedRow == this.tableView.Table.Rows.Count-1) {
+				if (this.tableView.SelectedRow == this.tableView.Table.Rows - 1) {
 					this.NavigateIf (k, Key.CursorDown, this.btnToggleSplitterCollapse);
 				}
 
@@ -318,7 +318,7 @@ namespace Terminal.Gui {
 			this.sorter = new FileDialogSorter (this, this.tableView);
 			this.history = new FileDialogHistory (this);
 
-			this.tableView.Table = this.dtFiles;
+			this.tableView.Table = new DataTableSource(this.dtFiles);
 
 			this.tbPath.TextChanged += (s, e) => this.PathChanged ();
 
@@ -514,7 +514,7 @@ namespace Terminal.Gui {
 
 		private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent)
 		{
-			if (tableView.Table.Rows.Count == 0) {
+			if (tableView.Table.Rows == 0) {
 				return;
 			}
 
@@ -539,11 +539,10 @@ namespace Terminal.Gui {
 		{
 			tableView.EnsureValidSelection ();
 			var col = tableView.SelectedColumn;
-			var style = tableView.Style.GetColumnStyleIfAny (tableView.Table.Columns [col]);
+			var style = tableView.Style.GetColumnStyleIfAny (col);
 
 
-			var collection = tableView
-				.Table
+			var collection = dtFiles
 				.Rows
 				.Cast<DataRow> ()
 				.Select ((o, idx) => col == 0 ? 
@@ -897,7 +896,7 @@ namespace Terminal.Gui {
 
 		private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEventArgs obj)
 		{
-			if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows.Count == 0) {
+			if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows == 0) {
 				return;
 			}
 
@@ -969,7 +968,7 @@ namespace Terminal.Gui {
 			this.dtFiles = new DataTable ();
 
 			var nameStyle = this.tableView.Style.GetOrCreateColumnStyle (
-				filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int))
+				filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int)).Ordinal
 				);
 			nameStyle.RepresentationGetter = (i) => {
 
@@ -989,11 +988,13 @@ namespace Terminal.Gui {
 
 			nameStyle.MinWidth = 50;
 
-			var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)));
+			var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (
+				this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)).Ordinal);
 			sizeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].HumanReadableLength ?? string.Empty;
 			nameStyle.MinWidth = 10;
 
-			var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)));
+			var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (
+				this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)).Ordinal);
 			dateModifiedStyle.RepresentationGetter = (i) => 
 			{
 				var s = this.State?.Children [(int)i];
@@ -1006,7 +1007,8 @@ namespace Terminal.Gui {
 
 			dateModifiedStyle.MinWidth = 30;
 
-			var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)));
+			var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (
+				this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)).Ordinal);
 			typeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].Type ?? string.Empty;
 			typeStyle.MinWidth = 6;
 
@@ -1237,7 +1239,7 @@ namespace Terminal.Gui {
 
 		private void BuildRow (int idx)
 		{
-			this.tableView.Table.Rows.Add (idx, idx, idx, idx);
+			dtFiles.Rows.Add (idx, idx, idx, idx);
 		}
 
 		private ColorScheme ColorGetter (TableView.CellColorGetterArgs args)
@@ -1277,7 +1279,7 @@ namespace Terminal.Gui {
 
 				foreach (var p in this.tableView.GetAllSelectedCells ()) {
 
-					var add = this.State?.Children [(int)this.tableView.Table.Rows [p.Y] [0]];
+					var add = this.State?.Children [(int)this.tableView.Table[p.Y, 0]];
 					if (add != null) {
 						toReturn.Add (add);
 					}
@@ -1288,7 +1290,7 @@ namespace Terminal.Gui {
 		}
 		private FileSystemInfoStats RowToStats (int rowIndex)
 		{
-			return this.State?.Children [(int)this.tableView.Table.Rows [rowIndex] [0]];
+			return this.State?.Children [(int)this.tableView.Table[rowIndex,0]];
 		}
 		private int? StatsToRow (IFileSystemInfo fileSystemInfo)
 		{
@@ -1299,7 +1301,7 @@ namespace Terminal.Gui {
 
 				// find the row number in our DataTable where the cell
 				// contains idx
-				var match = tableView.Table.Rows
+				var match = dtFiles.Rows
 					.Cast<DataRow> ()
 					.Select ((r, rIdx) => new { row = r, rowIdx = rIdx })
 					.Where (t => (int)t.row [0] == idx)
@@ -1367,7 +1369,7 @@ namespace Terminal.Gui {
 			private readonly FileDialog dlg;
 			private TableView tableView;
 
-			private DataColumn currentSort = null;
+			private int? currentSort = null;
 			private bool currentSortIsAsc = true;
 
 			public FileDialogSorter (FileDialog dlg, TableView tableView)
@@ -1378,17 +1380,17 @@ namespace Terminal.Gui {
 				// if user clicks the mouse in TableView
 				this.tableView.MouseClick += (s, e) => {
 
-					var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
+					var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
 
 					if (clickedCol != null) {
 						if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
 
 							// left click in a header
-							this.SortColumn (clickedCol);
+							this.SortColumn (clickedCol.Value);
 						} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
 
 							// right click in a header
-							this.ShowHeaderContextMenu (clickedCol, e);
+							this.ShowHeaderContextMenu (clickedCol.Value, e);
 						}
 					} else {
 						if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
@@ -1406,10 +1408,14 @@ namespace Terminal.Gui {
 			{
 				var col = this.currentSort;
 
+				if(col == null) {
+					return;
+				}
+
 				// TODO: Consider preserving selection
-				this.tableView.Table.Rows.Clear ();
+				dlg.dtFiles.Rows.Clear ();
 
-				var colName = col == null ? null : StripArrows (col.ColumnName);
+				var colName = col == null ? null : StripArrows (tableView.Table.ColumnNames[col.Value]);
 
 				var stats = this.dlg.State?.Children ?? new FileSystemInfoStats [0];
 
@@ -1438,13 +1444,13 @@ namespace Terminal.Gui {
 					this.dlg.BuildRow (o.i);
 				}
 
-				foreach (DataColumn c in this.tableView.Table.Columns) {
+				foreach (DataColumn c in dlg.dtFiles.Columns) {
 
 					// remove any lingering sort indicator
 					c.ColumnName = StripArrows (c.ColumnName);
 
 					// add a new one if this the one that is being sorted
-					if (c == col) {
+					if (c.Ordinal == col) {
 						c.ColumnName += this.currentSortIsAsc ? " (▲)" : " (▼)";
 					}
 				}
@@ -1458,13 +1464,13 @@ namespace Terminal.Gui {
 				return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
 			}
 
-			private void SortColumn (DataColumn clickedCol)
+			private void SortColumn (int clickedCol)
 			{
 				this.GetProposedNewSortOrder (clickedCol, out var isAsc);
 				this.SortColumn (clickedCol, isAsc);
 			}
 
-			internal void SortColumn (DataColumn col, bool isAsc)
+			internal void SortColumn (int col, bool isAsc)
 			{
 				// set a sort order
 				this.currentSort = col;
@@ -1473,19 +1479,19 @@ namespace Terminal.Gui {
 				this.ApplySort ();
 			}
 
-			private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc)
+			private string GetProposedNewSortOrder (int clickedCol, out bool isAsc)
 			{
 				// work out new sort order
 				if (this.currentSort == clickedCol && this.currentSortIsAsc) {
 					isAsc = false;
-					return $"{clickedCol.ColumnName} DESC";
+					return $"{tableView.Table.ColumnNames[clickedCol]} DESC";
 				} else {
 					isAsc = true;
-					return $"{clickedCol.ColumnName} ASC";
+					return $"{tableView.Table.ColumnNames [clickedCol]} ASC";
 				}
 			}
 
-			private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e)
+			private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e)
 			{
 				var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc);
 
@@ -1494,7 +1500,7 @@ namespace Terminal.Gui {
 					e.MouseEvent.Y + 1,
 					new MenuBarItem (new MenuItem []
 					{
-						new MenuItem($"Hide {StripArrows(clickedCol.ColumnName)}", string.Empty, () => this.HideColumn(clickedCol)),
+						new MenuItem($"Hide {StripArrows(tableView.Table.ColumnNames[clickedCol])}", string.Empty, () => this.HideColumn(clickedCol)),
 						new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)),
 					})
 				);
@@ -1524,7 +1530,7 @@ namespace Terminal.Gui {
 				contextMenu.Show ();
 			}
 
-			private void HideColumn (DataColumn clickedCol)
+			private void HideColumn (int clickedCol)
 			{
 				var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol);
 				style.Visible = false;

+ 2 - 2
Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs

@@ -11,7 +11,7 @@ namespace Terminal.Gui {
 		/// The current table to which the new indexes refer.  May be null e.g. if selection change is the result of clearing the table from the view
 		/// </summary>
 		/// <value></value>
-		public DataTable Table { get; }
+		public ITableSource Table { get; }
 
 		/// <summary>
 		/// The column index of the <see cref="Table"/> cell that is being activated
@@ -31,7 +31,7 @@ namespace Terminal.Gui {
 		/// <param name="t"></param>
 		/// <param name="col"></param>
 		/// <param name="row"></param>
-		public CellActivatedEventArgs (DataTable t, int col, int row)
+		public CellActivatedEventArgs (ITableSource t, int col, int row)
 		{
 			Table = t;
 			Col = col;

+ 35 - 0
Terminal.Gui/Views/TableView/DataTableSource.cs

@@ -0,0 +1,35 @@
+using System.Data;
+using System.Linq;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// <see cref="ITableSource"/> implementation that wraps 
+	/// a <see cref="System.Data.DataTable"/>.  This class is
+	/// mutable: changes are permitted to the wrapped <see cref="DataTable"/>.
+	/// </summary>
+	public class DataTableSource : ITableSource
+	{
+		private readonly DataTable table;
+
+		/// <summary>
+		/// Creates a new instance based on the data in <paramref name="table"/>.
+		/// </summary>
+		/// <param name="table"></param>
+		public DataTableSource(DataTable table)
+		{
+			this.table = table;
+		}
+
+		/// <inheritdoc/>
+		public object this [int row, int col] => table.Rows[row][col];
+
+		/// <inheritdoc/>
+		public int Rows => table.Rows.Count;
+
+		/// <inheritdoc/>
+		public int Columns => table.Columns.Count;
+
+		/// <inheritdoc/>
+		public string [] ColumnNames => table.Columns.Cast<DataColumn>().Select (c => c.ColumnName).ToArray ();
+	}
+}

+ 54 - 0
Terminal.Gui/Views/TableView/EnumerableTableSource.cs

@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// <see cref="ITableSource"/> implementation that wraps arbitrary data.
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	public class EnumerableTableSource<T> : ITableSource {
+		private T [] data;
+		private string [] cols;
+		private Dictionary<string, Func<T, object>> lamdas;
+
+		/// <summary>
+		/// Creates a new instance of the class that presents <paramref name="data"/>
+		/// collection as a table.
+		/// </summary>
+		/// <remarks>The elements of the <paramref name="data"/> collection are recorded during
+		/// construction (immutable) but the properties of those objects are permitted to
+		/// change.</remarks>
+		/// <param name="data">The data that you want to present.  The members of this collection
+		/// will be frozen after construction.</param>
+		/// <param name="columnDefinitions">
+		/// Getter methods for each property you want to present in the table. For example:
+		/// <code>
+		/// new () {
+		///    { "Colname1", (t)=>t.SomeField},
+		///    { "Colname2", (t)=>t.SomeOtherField}
+		///}
+		/// </code></param>
+		public EnumerableTableSource (IEnumerable<T> data, Dictionary<string, Func<T, object>> columnDefinitions)
+		{
+			this.data = data.ToArray ();
+			this.cols = columnDefinitions.Keys.ToArray ();
+			this.lamdas = columnDefinitions;
+		}
+
+		/// <inheritdoc/>
+		public object this [int row, int col] {
+			get => this.lamdas [ColumnNames [col]] (this.data [row]);
+		}
+
+		/// <inheritdoc/>
+		public int Rows => data.Length;
+
+		/// <inheritdoc/>
+		public int Columns => cols.Length;
+
+		/// <inheritdoc/>
+		public string [] ColumnNames => cols;
+	}
+}

+ 33 - 0
Terminal.Gui/Views/TableView/ITableSource.cs

@@ -0,0 +1,33 @@
+namespace Terminal.Gui {
+	/// <summary>
+	/// Tabular matrix of data to be displayed in a <see cref="TableView"/>.
+	/// </summary>
+	public interface ITableSource
+	{
+		/// <summary>
+		/// Gets the number of rows in the table.
+		/// </summary>
+		int Rows { get; }
+
+		/// <summary>
+		/// Gets the number of columns in the table.
+		/// </summary>
+		int Columns { get; }
+
+		/// <summary>
+		/// Gets the label for each column.
+		/// </summary>
+		string[] ColumnNames { get; }
+
+		/// <summary>
+		/// Returns the data at the given indexes of the table (row, column).
+		/// </summary>
+		/// <param name="row"></param>
+		/// <param name="col"></param>
+		/// <returns></returns>
+		object this[int row, int col]
+		{
+			get;
+		}
+	}
+}

+ 2 - 2
Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs

@@ -11,7 +11,7 @@ namespace Terminal.Gui {
 		/// The current table to which the new indexes refer.  May be null e.g. if selection change is the result of clearing the table from the view
 		/// </summary>
 		/// <value></value>
-		public DataTable Table { get; }
+		public ITableSource Table { get; }
 
 		/// <summary>
 		/// The previous selected column index.  May be invalid e.g. when the selection has been changed as a result of replacing the existing Table with a smaller one
@@ -45,7 +45,7 @@ namespace Terminal.Gui {
 		/// <param name="newCol"></param>
 		/// <param name="oldRow"></param>
 		/// <param name="newRow"></param>
-		public SelectedCellChangedEventArgs (DataTable t, int oldCol, int newCol, int oldRow, int newRow)
+		public SelectedCellChangedEventArgs (ITableSource t, int oldCol, int newCol, int oldRow, int newRow)
 		{
 			Table = t;
 			OldCol = oldCol;

+ 88 - 78
Terminal.Gui/Views/TableView/TableView.cs

@@ -7,19 +7,18 @@ using System.Linq;
 
 namespace Terminal.Gui {
 
-
 	/// <summary>
-	/// View for tabular data based on a <see cref="DataTable"/>.
+	/// View for tabular data based on a <see cref="ITableSource"/>.
 	/// 
 	/// <a href="https://gui-cs.github.io/Terminal.Gui/articles/tableview.html">See TableView Deep Dive for more information</a>.
 	/// </summary>
-	public partial class TableView : View {
+	public class TableView : View {
 
 		private int columnOffset;
 		private int rowOffset;
 		private int selectedRow;
 		private int selectedColumn;
-		private DataTable table;
+		private ITableSource table;
 		private TableStyle style = new TableStyle ();
 		private Key cellActivationKey = Key.Enter;
 
@@ -39,7 +38,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The data table to render in the view.  Setting this property automatically updates and redraws the control.
 		/// </summary>
-		public DataTable Table { get => table; set { table = value; Update (); } }
+		public ITableSource Table { get => table; set { table = value; Update (); } }
 
 		/// <summary>
 		/// Contains options for changing how the table is rendered
@@ -71,7 +70,7 @@ namespace Terminal.Gui {
 			get => columnOffset;
 
 			//try to prevent this being set to an out of bounds column
-			set => columnOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value));
+			set => columnOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Columns - 1, value));
 		}
 
 		/// <summary>
@@ -79,7 +78,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		public int RowOffset {
 			get => rowOffset;
-			set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value));
+			set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows - 1, value));
 		}
 
 		/// <summary>
@@ -92,7 +91,7 @@ namespace Terminal.Gui {
 				var oldValue = selectedColumn;
 
 				//try to prevent this being set to an out of bounds column
-				selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
+				selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns - 1, Math.Max (0, value));
 
 				if (oldValue != selectedColumn)
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow));
@@ -108,7 +107,7 @@ namespace Terminal.Gui {
 
 				var oldValue = selectedRow;
 
-				selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
+				selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows - 1, Math.Max (0, value));
 
 				if (oldValue != selectedRow)
 					OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow));
@@ -161,7 +160,7 @@ namespace Terminal.Gui {
 		/// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. 
 		/// </summary>
 		/// <param name="table">The table to display in the control</param>
-		public TableView (DataTable table) : this ()
+		public TableView (ITableSource table) : this ()
 		{
 			this.Table = table;
 		}
@@ -231,8 +230,9 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override void Redraw (Rect bounds)
 		{
+			base.Redraw (bounds);
+
 			Move (0, 0);
-			var frame = Frame;
 
 			scrollRightPoint = null;
 			scrollLeftPoint = null;
@@ -273,7 +273,7 @@ namespace Terminal.Gui {
 			int headerLinesConsumed = line;
 
 			//render the cells
-			for (; line < frame.Height; line++) {
+			for (; line < Bounds.Height; line++) {
 
 				ClearLine (line, bounds.Width);
 
@@ -285,11 +285,12 @@ namespace Terminal.Gui {
 					continue;
 
 				// No more data
-				if(rowToRender >= Table.Rows.Count) {
+				if(rowToRender >= Table.Rows) {
 
-					if(rowToRender == Table.Rows.Count && Style.ShowHorizontalBottomline) {
+					if(rowToRender == Table.Rows && Style.ShowHorizontalBottomline) {
 						RenderBottomLine (line, bounds.Width, columnsToRender);
 					}
+
 					continue;
 				}
 
@@ -382,7 +383,7 @@ namespace Terminal.Gui {
 				var current = columnsToRender [i];
 
 				var colStyle = Style.GetColumnStyleIfAny (current.Column);
-				var colName = current.Column.ColumnName;
+				var colName = table.ColumnNames[current.Column];
 
 				RenderSeparator (current.X - 1, row, true);
 
@@ -420,7 +421,7 @@ namespace Terminal.Gui {
 			int lastColumnIdxRendered = ColumnOffset + columnsToRender.Length - 1;
 
 			// are there more valid indexes?
-			bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns.Count;
+			bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns;
 
 			// if we went right from the last column would we find a new visible column?
 			if (!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) {
@@ -555,9 +556,9 @@ namespace Terminal.Gui {
 				Move (current.X, row);
 
 				// Set color scheme based on whether the current cell is the selected one
-				bool isSelectedCell = IsSelected (current.Column.Ordinal, rowToRender);
+				bool isSelectedCell = IsSelected (current.Column, rowToRender);
 
-				var val = Table.Rows [rowToRender] [current.Column];
+				var val = Table [rowToRender, current.Column];
 
 				// Render the (possibly truncated) cell value
 				var representation = GetRepresentation (val, colStyle);
@@ -569,7 +570,7 @@ namespace Terminal.Gui {
 				if (colorSchemeGetter != null) {
 					// user has a delegate for defining row color per cell, call it
 					scheme = colorSchemeGetter (
-						new CellColorGetterArgs (Table, rowToRender, current.Column.Ordinal, val, representation, rowScheme));
+						new CellColorGetterArgs (Table, rowToRender, current.Column, val, representation, rowScheme));
 
 					// if users custom color getter returned null, use the row scheme
 					if (scheme == null) {
@@ -590,7 +591,7 @@ namespace Terminal.Gui {
 				var render = TruncateOrPad (val, representation, current.Width, colStyle);
 
 				// While many cells can be selected (see MultiSelectedRegions) only one cell is the primary (drives navigation etc)
-				bool isPrimaryCell = current.Column.Ordinal == selectedColumn && rowToRender == selectedRow;
+				bool isPrimaryCell = current.Column == selectedColumn && rowToRender == selectedRow;
 
 				RenderCell (cellColor, render, isPrimaryCell);
 
@@ -880,9 +881,9 @@ namespace Terminal.Gui {
 		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
 		public void ChangeSelectionToEndOfTable (bool extend)
 		{
-			var finalColumn = Table.Columns.Count - 1;
+			var finalColumn = Table.Columns - 1;
 
-			SetSelection (FullRowSelect ? SelectedColumn : finalColumn, Table.Rows.Count - 1, extend);
+			SetSelection (FullRowSelect ? SelectedColumn : finalColumn, Table.Rows - 1, extend);
 			Update ();
 		}
 
@@ -892,7 +893,7 @@ namespace Terminal.Gui {
 		/// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
 		public void ChangeSelectionToEndOfRow (bool extend)
 		{
-			SetSelection (Table.Columns.Count - 1, SelectedRow, extend);
+			SetSelection (Table.Columns - 1, SelectedRow, extend);
 			Update ();
 		}
 
@@ -911,13 +912,13 @@ namespace Terminal.Gui {
 		/// </summary>
 		public void SelectAll ()
 		{
-			if (TableIsNullOrInvisible () || !MultiSelect || Table.Rows.Count == 0)
+			if (TableIsNullOrInvisible () || !MultiSelect || Table.Rows == 0)
 				return;
 
 			ClearMultiSelectedRegions (true);
 
 			// Create a single region over entire table, set the origin of the selection to the active cell so that a followup spread selection e.g. shift-right behaves properly
-			MultiSelectedRegions.Push (new TableSelection (new Point (SelectedColumn, SelectedRow), new Rect (0, 0, Table.Columns.Count, table.Rows.Count)));
+			MultiSelectedRegions.Push (new TableSelection (new Point (SelectedColumn, SelectedRow), new Rect (0, 0, Table.Columns, table.Rows)));
 			Update ();
 		}
 
@@ -927,7 +928,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public IEnumerable<Point> GetAllSelectedCells ()
 		{
-			if (TableIsNullOrInvisible () || Table.Rows.Count == 0)
+			if (TableIsNullOrInvisible () || Table.Rows == 0)
 			{
 				return Enumerable.Empty<Point>();				
 			}
@@ -944,7 +945,7 @@ namespace Terminal.Gui {
 				var yMax = MultiSelectedRegions.Max (r => r.Rect.Bottom);
 
 				var xMin = FullRowSelect ? 0 : MultiSelectedRegions.Min (r => r.Rect.Left);
-				var xMax = FullRowSelect ? Table.Columns.Count : MultiSelectedRegions.Max (r => r.Rect.Right);
+				var xMax = FullRowSelect ? Table.Columns : MultiSelectedRegions.Max (r => r.Rect.Right);
 
 				for (int y = yMin; y < yMax; y++) {
 					for (int x = xMin; x < xMax; x++) {
@@ -960,7 +961,7 @@ namespace Terminal.Gui {
 			// if we are selecting the full row
 			if (FullRowSelect) {
 				// all cells in active row are selected
-				for (int x = 0; x < Table.Columns.Count; x++) {
+				for (int x = 0; x < Table.Columns; x++) {
 					toReturn.Add(new Point (x, SelectedRow));
 				}
 			} else {
@@ -1094,11 +1095,11 @@ namespace Terminal.Gui {
 		private bool IsColumnVisible (int columnIndex)
 		{
 			// if the column index provided is out of bounds
-			if (columnIndex < 0 || columnIndex >= table.Columns.Count) {
+			if (columnIndex < 0 || columnIndex >= table.Columns) {
 				return false;
 			}
 
-			return this.Style.GetColumnStyleIfAny (Table.Columns [columnIndex])?.Visible ?? true;
+			return this.Style.GetColumnStyleIfAny (columnIndex)?.Visible ?? true;
 		}
 
 		/// <summary>
@@ -1160,25 +1161,29 @@ namespace Terminal.Gui {
 				return true;
 			}
 
+			// TODO: Revert this (or not) once #2578 is solved
+			var boundsX = me.X - GetFramesThickness ().Left;
+			var boundsY = me.Y - GetFramesThickness ().Top;
+
 			if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
 
 				if (scrollLeftPoint != null
-					&& scrollLeftPoint.Value.X == me.X
-					&& scrollLeftPoint.Value.Y == me.Y) {
+					&& scrollLeftPoint.Value.X == boundsX
+					&& scrollLeftPoint.Value.Y == boundsY) {
 					ColumnOffset--;
 					EnsureValidScrollOffsets ();
 					SetNeedsDisplay ();
 				}
 
 				if (scrollRightPoint != null
-					&& scrollRightPoint.Value.X == me.X
-					&& scrollRightPoint.Value.Y == me.Y) {
+					&& scrollRightPoint.Value.X == boundsX
+					&& scrollRightPoint.Value.Y == boundsY) {
 					ColumnOffset++;
 					EnsureValidScrollOffsets ();
 					SetNeedsDisplay ();
 				}
 
-				var hit = ScreenToCell (me.X, me.Y);
+				var hit = ScreenToCell (boundsX, boundsY);
 				if (hit != null) {
 
 					if (MultiSelect && HasControlOrAlt (me)) {
@@ -1193,7 +1198,7 @@ namespace Terminal.Gui {
 
 			// Double clicking a cell activates
 			if (me.Flags == MouseFlags.Button1DoubleClicked) {
-				var hit = ScreenToCell (me.X, me.Y);
+				var hit = ScreenToCell (boundsX, boundsY);
 				if (hit != null) {
 					OnCellActivated (new CellActivatedEventArgs (Table, hit.Value.X, hit.Value.Y));
 				}
@@ -1224,7 +1229,7 @@ namespace Terminal.Gui {
 		/// <param name="clientX">X offset from the top left of the control.</param>
 		/// <param name="clientY">Y offset from the top left of the control.</param>
 		/// <param name="headerIfAny">If the click is in a header this is the column clicked.</param>
-		public Point? ScreenToCell (int clientX, int clientY, out DataColumn headerIfAny)
+		public Point? ScreenToCell (int clientX, int clientY, out int? headerIfAny)
 		{
 			headerIfAny = null;
 
@@ -1247,13 +1252,13 @@ namespace Terminal.Gui {
 
 			// if click is off bottom of the rows don't give an
 			// invalid index back to user!
-			if (rowIdx >= Table.Rows.Count) {
+			if (rowIdx >= Table.Rows) {
 				return null;
 			}
 
 			if (col != null && rowIdx >= 0) {
 
-				return new Point (col.Column.Ordinal, rowIdx);
+				return new Point (col.Column, rowIdx);
 			}
 
 			return null;
@@ -1262,7 +1267,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Returns the screen position (relative to the control client area) that the given cell is rendered or null if it is outside the current scroll area or no table is loaded
 		/// </summary>
-		/// <param name="tableColumn">The index of the <see cref="Table"/> column you are looking for, use <see cref="DataColumn.Ordinal"/></param>
+		/// <param name="tableColumn">The index of the <see cref="Table"/> column you are looking for</param>
 		/// <param name="tableRow">The index of the row in <see cref="Table"/> that you are looking for</param>
 		/// <returns></returns>
 		public Point? CellToScreen (int tableColumn, int tableRow)
@@ -1274,7 +1279,7 @@ namespace Terminal.Gui {
 
 			var headerHeight = GetHeaderHeightIfAny ();
 
-			var colHit = viewPort.FirstOrDefault (c => c.Column.Ordinal == tableColumn);
+			var colHit = viewPort.FirstOrDefault (c => c.Column == tableColumn);
 
 			// current column is outside the scroll area
 			if (colHit == null)
@@ -1319,8 +1324,8 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			ColumnOffset = Math.Max (Math.Min (ColumnOffset, Table.Columns.Count - 1), 0);
-			RowOffset = Math.Max (Math.Min (RowOffset, Table.Rows.Count - 1), 0);
+			ColumnOffset = Math.Max (Math.Min (ColumnOffset, Table.Columns - 1), 0);
+			RowOffset = Math.Max (Math.Min (RowOffset, Table.Rows - 1), 0);
 		}
 
 		/// <summary>
@@ -1336,8 +1341,8 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0);
-			SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.Count - 1), 0);
+			SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns - 1), 0);
+			SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows - 1), 0);
 
 			// If SelectedColumn is invisible move it to a visible one
 			SelectedColumn = GetNearestVisibleColumn (SelectedColumn, lookRight: true, true);
@@ -1349,23 +1354,23 @@ namespace Terminal.Gui {
 			// evaluate 
 			foreach (var region in oldRegions) {
 				// ignore regions entirely below current table state
-				if (region.Rect.Top >= Table.Rows.Count)
+				if (region.Rect.Top >= Table.Rows)
 					continue;
 
 				// ignore regions entirely too far right of table columns
-				if (region.Rect.Left >= Table.Columns.Count)
+				if (region.Rect.Left >= Table.Columns)
 					continue;
 
 				// ensure region's origin exists
 				region.Origin = new Point (
-					Math.Max (Math.Min (region.Origin.X, Table.Columns.Count - 1), 0),
-					Math.Max (Math.Min (region.Origin.Y, Table.Rows.Count - 1), 0));
+					Math.Max (Math.Min (region.Origin.X, Table.Columns - 1), 0),
+					Math.Max (Math.Min (region.Origin.Y, Table.Rows - 1), 0));
 
 				// ensure regions do not go over edge of table bounds
 				region.Rect = Rect.FromLTRB (region.Rect.Left,
 					region.Rect.Top,
-					Math.Max (Math.Min (region.Rect.Right, Table.Columns.Count), 0),
-					Math.Max (Math.Min (region.Rect.Bottom, Table.Rows.Count), 0)
+					Math.Max (Math.Min (region.Rect.Right, Table.Columns), 0),
+					Math.Max (Math.Min (region.Rect.Bottom, Table.Rows), 0)
 					);
 
 				MultiSelectedRegions.Push (region);
@@ -1374,7 +1379,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// Returns true if the <see cref="Table"/> is not set or all the
-		/// <see cref="DataColumn"/> in the <see cref="Table"/> have an explicit
+		/// columns in the <see cref="Table"/> have an explicit
 		/// <see cref="ColumnStyle"/> that marks them <see cref="ColumnStyle.visible"/>
 		/// <see langword="false"/>.
 		/// </summary>
@@ -1382,14 +1387,14 @@ namespace Terminal.Gui {
 		private bool TableIsNullOrInvisible ()
 		{
 			return Table == null ||
-				Table.Columns.Count <= 0 ||
-				Table.Columns.Cast<DataColumn> ().All (
+				Table.Columns <= 0 ||
+				Enumerable.Range(0,Table.Columns).All (
 				c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false);
 		}
 
 		/// <summary>
 		/// Returns <paramref name="columnIndex"/> unless the <see cref="ColumnStyle.Visible"/> is false for
-		/// the indexed <see cref="DataColumn"/>.  If so then the index returned is nudged to the nearest visible
+		/// the indexed column.  If so then the index returned is nudged to the nearest visible
 		/// column.
 		/// </summary>
 		/// <remarks>Returns <paramref name="columnIndex"/> unchanged if it is invalid (e.g. out of bounds).</remarks>
@@ -1412,14 +1417,15 @@ namespace Terminal.Gui {
 		private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection, out int idx)
 		{
 			// if the column index provided is out of bounds
-			if (columnIndex < 0 || columnIndex >= table.Columns.Count) {
+			if (columnIndex < 0 || columnIndex >= table.Columns) {
 
 				idx = columnIndex;
 				return false;
 			}
 
 			// get the column visibility by index (if no style visible is true)
-			bool [] columnVisibility = Table.Columns.Cast<DataColumn> ()
+			bool [] columnVisibility = 
+				Enumerable.Range(0,Table.Columns)
 				.Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true)
 				.ToArray ();
 
@@ -1470,7 +1476,7 @@ namespace Terminal.Gui {
 		/// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
 		public void EnsureSelectedCellIsVisible ()
 		{
-			if (Table == null || Table.Columns.Count <= 0) {
+			if (Table == null || Table.Columns <= 0) {
 				return;
 			}
 
@@ -1478,24 +1484,24 @@ namespace Terminal.Gui {
 			var headerHeight = GetHeaderHeightIfAny ();
 
 			//if we have scrolled too far to the left 
-			if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) {
+			if (SelectedColumn < columnsToRender.Min (r => r.Column)) {
 				ColumnOffset = SelectedColumn;
 			}
 
 			//if we have scrolled too far to the right
-			if (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) {
+			if (SelectedColumn > columnsToRender.Max (r => r.Column)) {
 
 				if (Style.SmoothHorizontalScrolling) {
 
 					// Scroll right 1 column at a time until the users selected column is visible
-					while (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) {
+					while (SelectedColumn > columnsToRender.Max (r => r.Column)) {
 
 						ColumnOffset++;
 						columnsToRender = CalculateViewport (Bounds).ToArray ();
 
 						// if we are already scrolled to the last column then break
 						// this will prevent any theoretical infinite loop
-						if (ColumnOffset >= Table.Columns.Count - 1)
+						if (ColumnOffset >= Table.Columns - 1)
 							break;
 
 					}
@@ -1559,9 +1565,10 @@ namespace Terminal.Gui {
 				rowsToRender -= GetHeaderHeight ();
 
 			bool first = true;
-			var lastColumn = Table.Columns.Cast<DataColumn> ().Last ();
+			var lastColumn = Table.Columns - 1;
 
-			foreach (var col in Table.Columns.Cast<DataColumn> ().Skip (ColumnOffset)) {
+			// TODO : Maybe just a for loop?
+			foreach (var col in Enumerable.Range(0,Table.Columns).Skip (ColumnOffset)) {
 
 				int startingIdxForCurrentHeader = usedSpace;
 				var colStyle = Style.GetColumnStyleIfAny (col);
@@ -1639,18 +1646,21 @@ namespace Terminal.Gui {
 		/// <param name="rowsToRender"></param>
 		/// <param name="colStyle"></param>
 		/// <returns></returns>
-		private int CalculateMaxCellWidth (DataColumn col, int rowsToRender, ColumnStyle colStyle)
+		private int CalculateMaxCellWidth (int col, int rowsToRender, ColumnStyle colStyle)
 		{
-			int spaceRequired = col.ColumnName.Sum (c => Rune.ColumnWidth (c));
+			int spaceRequired = table.ColumnNames[col].Sum (c => Rune.ColumnWidth (c));
 
 			// if table has no rows
 			if (RowOffset < 0)
 				return spaceRequired;
 
-			for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
+
+			for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows; i++) {
 
 				//expand required space if cell is bigger than the last biggest cell or header
-				spaceRequired = Math.Max (spaceRequired, GetRepresentation (Table.Rows [i] [col], colStyle).Sum (c => Rune.ColumnWidth (c)));
+				spaceRequired = Math.Max (
+					spaceRequired,
+					GetRepresentation (Table [i,col], colStyle).Sum (c => Rune.ColumnWidth (c)));
 			}
 
 			// Don't require more space than the style allows
@@ -1874,7 +1884,7 @@ namespace Terminal.Gui {
 			/// <summary>
 			/// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc)
 			/// </summary>
-			public Dictionary<DataColumn, ColumnStyle> ColumnStyles { get; set; } = new Dictionary<DataColumn, ColumnStyle> ();
+			public Dictionary<int, ColumnStyle> ColumnStyles { get; set; } = new Dictionary<int, ColumnStyle> ();
 
 			/// <summary>
 			/// Delegate for coloring specific rows in a different color.  For cell color <see cref="ColumnStyle.ColorGetter"/>
@@ -1913,7 +1923,7 @@ namespace Terminal.Gui {
 			/// </summary>
 			/// <param name="col"></param>
 			/// <returns></returns>
-			public ColumnStyle GetColumnStyleIfAny (DataColumn col)
+			public ColumnStyle GetColumnStyleIfAny (int col)
 			{
 				return ColumnStyles.TryGetValue (col, out ColumnStyle result) ? result : null;
 			}
@@ -1923,7 +1933,7 @@ namespace Terminal.Gui {
 			/// </summary>
 			/// <param name="col"></param>
 			/// <returns></returns>
-			public ColumnStyle GetOrCreateColumnStyle (DataColumn col)
+			public ColumnStyle GetOrCreateColumnStyle (int col)
 			{
 				if (!ColumnStyles.ContainsKey (col))
 					ColumnStyles.Add (col, new ColumnStyle ());
@@ -1940,7 +1950,7 @@ namespace Terminal.Gui {
 			/// <summary>
 			/// The column to render
 			/// </summary>
-			public DataColumn Column { get; set; }
+			public int Column { get; set; }
 
 			/// <summary>
 			/// The horizontal position to begin rendering the column at
@@ -1958,7 +1968,7 @@ namespace Terminal.Gui {
 			/// </summary>
 			public bool IsVeryLast { get; }
 
-			public ColumnToRender (DataColumn col, int x, int width, bool isVeryLast)
+			public ColumnToRender (int col, int x, int width, bool isVeryLast)
 			{
 				Column = col;
 				X = x;
@@ -1977,7 +1987,7 @@ namespace Terminal.Gui {
 			/// <summary>
 			/// The data table hosted by the <see cref="TableView"/> control.
 			/// </summary>
-			public DataTable Table { get; }
+			public ITableSource Table { get; }
 
 			/// <summary>
 			/// The index of the row in <see cref="Table"/> for which color is needed
@@ -2004,7 +2014,7 @@ namespace Terminal.Gui {
 			/// </summary>
 			public ColorScheme RowScheme { get; }
 
-			internal CellColorGetterArgs (DataTable table, int rowIdx, int colIdx, object cellValue, string representation, ColorScheme rowScheme)
+			internal CellColorGetterArgs (ITableSource table, int rowIdx, int colIdx, object cellValue, string representation, ColorScheme rowScheme)
 			{
 				Table = table;
 				RowIndex = rowIdx;
@@ -2017,7 +2027,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Arguments for <see cref="RowColorGetterDelegate"/>. Describes a row of data in a <see cref="DataTable"/>
+		/// Arguments for <see cref="RowColorGetterDelegate"/>. Describes a row of data in a <see cref="ITableSource"/>
 		/// for which <see cref="ColorScheme"/> is sought.
 		/// </summary>
 		public class RowColorGetterArgs {
@@ -2025,14 +2035,14 @@ namespace Terminal.Gui {
 			/// <summary>
 			/// The data table hosted by the <see cref="TableView"/> control.
 			/// </summary>
-			public DataTable Table { get; }
+			public ITableSource Table { get; }
 
 			/// <summary>
 			/// The index of the row in <see cref="Table"/> for which color is needed
 			/// </summary>
 			public int RowIndex { get; }
 
-			internal RowColorGetterArgs (DataTable table, int rowIdx)
+			internal RowColorGetterArgs (ITableSource table, int rowIdx)
 			{
 				Table = table;
 				RowIndex = rowIdx;

+ 36 - 34
UICatalog/Scenarios/CsvEditor.cs

@@ -22,6 +22,7 @@ namespace UICatalog.Scenarios {
 	public class CsvEditor : Scenario {
 		TableView tableView;
 		private string currentFile;
+		DataTable currentTable;
 		private MenuItem miLeft;
 		private MenuItem miRight;
 		private MenuItem miCentered;
@@ -121,9 +122,7 @@ namespace UICatalog.Scenarios {
 			if (tableView.Table == null || tableView.SelectedColumn == -1)
 				return;
 
-			var col = tableView.Table.Columns [tableView.SelectedColumn];
-
-			var style = tableView.Style.GetColumnStyleIfAny (col);
+			var style = tableView.Style.GetColumnStyleIfAny (tableView.SelectedColumn);
 
 			miLeft.Checked = style?.Alignment == TextAlignment.Left;
 			miRight.Checked = style?.Alignment == TextAlignment.Right;
@@ -136,7 +135,7 @@ namespace UICatalog.Scenarios {
 				return;
 			}
 
-			var currentCol = tableView.Table.Columns [tableView.SelectedColumn];
+			var currentCol = currentTable.Columns [tableView.SelectedColumn];
 
 			if (GetText ("Rename Column", "Name:", currentCol.ColumnName, out string newName)) {
 				currentCol.ColumnName = newName;
@@ -157,7 +156,7 @@ namespace UICatalog.Scenarios {
 			}
 
 			try {
-				tableView.Table.Columns.RemoveAt (tableView.SelectedColumn);
+				currentTable.Columns.RemoveAt (tableView.SelectedColumn);
 				tableView.Update ();
 
 			} catch (Exception ex) {
@@ -179,11 +178,11 @@ namespace UICatalog.Scenarios {
 
 			try {
 
-				var currentCol = tableView.Table.Columns [tableView.SelectedColumn];
+				var currentCol = currentTable.Columns [tableView.SelectedColumn];
 
 				if (GetText ("Move Column", "New Index:", currentCol.Ordinal.ToString (), out string newOrdinal)) {
 
-					var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Columns.Count - 1);
+					var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Columns - 1);
 
 					currentCol.SetOrdinal (newIdx);
 
@@ -209,10 +208,15 @@ namespace UICatalog.Scenarios {
 				return;
 			}
 
-			var colName = tableView.Table.Columns [tableView.SelectedColumn].ColumnName;
+			var colName = tableView.Table.ColumnNames [tableView.SelectedColumn];
+
+			currentTable.DefaultView.Sort = colName + (asc ? " asc" : " desc");
+			SetTable(currentTable.DefaultView.ToTable ());
+		}
 
-			tableView.Table.DefaultView.Sort = colName + (asc ? " asc" : " desc");
-			tableView.Table = tableView.Table.DefaultView.ToTable ();
+		private void SetTable (DataTable dataTable)
+		{			
+			tableView.Table = new DataTableSource(currentTable = dataTable);
 		}
 
 		private void MoveRow ()
@@ -231,23 +235,23 @@ namespace UICatalog.Scenarios {
 
 				int oldIdx = tableView.SelectedRow;
 
-				var currentRow = tableView.Table.Rows [oldIdx];
+				var currentRow = currentTable.Rows [oldIdx];
 
 				if (GetText ("Move Row", "New Row:", oldIdx.ToString (), out string newOrdinal)) {
 
-					var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Rows.Count - 1);
+					var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Rows - 1);
 
 					if (newIdx == oldIdx)
 						return;
 
 					var arrayItems = currentRow.ItemArray;
-					tableView.Table.Rows.Remove (currentRow);
+					currentTable.Rows.Remove (currentRow);
 
 					// Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance
-					var newRow = tableView.Table.NewRow ();
+					var newRow = currentTable.NewRow ();
 					newRow.ItemArray = arrayItems;
 
-					tableView.Table.Rows.InsertAt (newRow, newIdx);
+					currentTable.Rows.InsertAt (newRow, newIdx);
 
 					tableView.SetSelection (tableView.SelectedColumn, newIdx, false);
 					tableView.EnsureSelectedCellIsVisible ();
@@ -265,9 +269,7 @@ namespace UICatalog.Scenarios {
 				return;
 			}
 
-			var col = tableView.Table.Columns [tableView.SelectedColumn];
-
-			var style = tableView.Style.GetOrCreateColumnStyle (col);
+			var style = tableView.Style.GetOrCreateColumnStyle (tableView.SelectedColumn);
 			style.Alignment = newAlignment;
 
 			miLeft.Checked = style.Alignment == TextAlignment.Left;
@@ -283,14 +285,14 @@ namespace UICatalog.Scenarios {
 				return;
 			}
 
-			var col = tableView.Table.Columns [tableView.SelectedColumn];
+			var col = currentTable.Columns [tableView.SelectedColumn];
 
 			if (col.DataType == typeof (string)) {
 				MessageBox.ErrorQuery ("Cannot Format Column", "String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type", "Ok");
 				return;
 			}
 
-			var style = tableView.Style.GetOrCreateColumnStyle (col);
+			var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal);
 
 			if (GetText ("Format", "Pattern:", style.Format ?? "", out string newPattern)) {
 				style.Format = newPattern;
@@ -314,11 +316,11 @@ namespace UICatalog.Scenarios {
 				return;
 			}
 
-			var newRow = tableView.Table.NewRow ();
+			var newRow = currentTable.NewRow ();
 
-			var newRowIdx = Math.Min (Math.Max (0, tableView.SelectedRow + 1), tableView.Table.Rows.Count);
+			var newRowIdx = Math.Min (Math.Max (0, tableView.SelectedRow + 1), tableView.Table.Rows);
 
-			tableView.Table.Rows.InsertAt (newRow, newRowIdx);
+			currentTable.Rows.InsertAt (newRow, newRowIdx);
 			tableView.Update ();
 		}
 
@@ -332,7 +334,7 @@ namespace UICatalog.Scenarios {
 
 				var col = new DataColumn (colName);
 
-				var newColIdx = Math.Min (Math.Max (0, tableView.SelectedColumn + 1), tableView.Table.Columns.Count);
+				var newColIdx = Math.Min (Math.Max (0, tableView.SelectedColumn + 1), tableView.Table.Columns);
 
 				int result = MessageBox.Query ("Column Type", "Pick a data type for the column", new ustring [] { "Date", "Integer", "Double", "Text", "Cancel" });
 
@@ -353,7 +355,7 @@ namespace UICatalog.Scenarios {
 					break;
 				}
 
-				tableView.Table.Columns.Add (col);
+				currentTable.Columns.Add (col);
 				col.SetOrdinal (newColIdx);
 				tableView.Update ();
 			}
@@ -371,13 +373,13 @@ namespace UICatalog.Scenarios {
 				new StreamWriter (File.OpenWrite (currentFile)),
 				CultureInfo.InvariantCulture);
 
-			foreach (var col in tableView.Table.Columns.Cast<DataColumn> ().Select (c => c.ColumnName)) {
+			foreach (var col in currentTable.Columns.Cast<DataColumn> ().Select (c => c.ColumnName)) {
 				writer.WriteField (col);
 			}
 
 			writer.NextRecord ();
 
-			foreach (DataRow row in tableView.Table.Rows) {
+			foreach (DataRow row in currentTable.Rows) {
 				foreach (var item in row.ItemArray) {
 					writer.WriteField (item);
 				}
@@ -428,7 +430,7 @@ namespace UICatalog.Scenarios {
 					}
 				}
 
-				tableView.Table = dt;
+				SetTable(dt);
 
 				// Only set the current filename if we successfully loaded the entire file
 				currentFile = filename;
@@ -459,7 +461,7 @@ namespace UICatalog.Scenarios {
 			};*/
 
 			tableView.DrawContent += (s, e) => {
-				_scrollBar.Size = tableView.Table?.Rows?.Count ?? 0;
+				_scrollBar.Size = tableView.Table?.Rows ?? 0;
 				_scrollBar.Position = tableView.RowOffset;
 				//	_scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1;
 				//	_scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
@@ -475,12 +477,12 @@ namespace UICatalog.Scenarios {
 				if (tableView.FullRowSelect) {
 					// Delete button deletes all rows when in full row mode
 					foreach (int toRemove in tableView.GetAllSelectedCells ().Select (p => p.Y).Distinct ().OrderByDescending (i => i))
-						tableView.Table.Rows.RemoveAt (toRemove);
+						currentTable.Rows.RemoveAt (toRemove);
 				} else {
 
 					// otherwise set all selected cells to null
 					foreach (var pt in tableView.GetAllSelectedCells ()) {
-						tableView.Table.Rows [pt.Y] [pt.X] = DBNull.Value;
+						currentTable.Rows [pt.Y] [pt.X] = DBNull.Value;
 					}
 				}
 
@@ -540,11 +542,11 @@ namespace UICatalog.Scenarios {
 			if (e.Table == null)
 				return;
 
-			var oldValue = e.Table.Rows [e.Row] [e.Col].ToString ();
+			var oldValue = currentTable.Rows [e.Row] [e.Col].ToString ();
 
-			if (GetText ("Enter new value", e.Table.Columns [e.Col].ColumnName, oldValue, out string newText)) {
+			if (GetText ("Enter new value", currentTable.Columns [e.Col].ColumnName, oldValue, out string newText)) {
 				try {
-					e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText;
+					currentTable.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText;
 				} catch (Exception ex) {
 					MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok");
 				}

+ 5 - 4
UICatalog/Scenarios/MultiColouredTable.cs

@@ -10,6 +10,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("TableView")]
 	public class MultiColouredTable : Scenario {
 		TableViewColors tableView;
+		private DataTable table;
 
 		public override void Setup ()
 		{
@@ -60,7 +61,7 @@ namespace UICatalog.Scenarios {
 				Normal = Application.Driver.MakeAttribute (Color.DarkGray, Color.Black)
 			};
 
-			tableView.Table = dt;
+			tableView.Table = new DataTableSource(this.table = dt);
 		}
 				
 		private void Quit ()
@@ -103,11 +104,11 @@ namespace UICatalog.Scenarios {
 			if (e.Table == null)
 				return;
 
-			var oldValue = e.Table.Rows [e.Row] [e.Col].ToString ();
+			var oldValue = e.Table[e.Row, e.Col].ToString ();
 
-			if (GetText ("Enter new value", e.Table.Columns [e.Col].ColumnName, oldValue, out string newText)) {
+			if (GetText ("Enter new value", e.Table.ColumnNames [e.Col], oldValue, out string newText)) {
 				try {
-					e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText;
+					table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText;
 				} catch (Exception ex) {
 					MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok");
 				}

+ 62 - 0
UICatalog/Scenarios/ProcessTable.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using Terminal.Gui;
+using System.Linq;
+using System.Globalization;
+using static Terminal.Gui.TableView;
+using System.Diagnostics;
+
+namespace UICatalog.Scenarios {
+
+	[ScenarioMetadata (Name: "ProcessTable", Description: "Demonstrates TableView with the currently running processes.")]
+	[ScenarioCategory ("TableView")]
+	public class ProcessTable : Scenario {
+		TableView tableView;
+		public override void Setup ()
+		{
+			Win.Title = this.GetName ();
+			Win.Y = 1; // menu
+			Win.Height = Dim.Fill (1); // status bar
+			Application.Top.LayoutSubviews ();
+
+			this.tableView = new TableView () {
+				X = 0,
+				Y = 0,
+				Width = Dim.Fill (),
+				Height = Dim.Fill (1),
+			};
+
+			// First time
+			CreateProcessTable ();
+
+			// Then every second
+			Application.MainLoop.AddTimeout (TimeSpan.FromSeconds (1),
+				(s) => {
+					CreateProcessTable ();
+					return true;
+				});
+
+			Win.Add (tableView);
+
+		}
+
+		private void CreateProcessTable ()
+		{
+			var ro = tableView.RowOffset;
+			var co = tableView.ColumnOffset;
+			tableView.Table = new EnumerableTableSource<Process> (Process.GetProcesses (),
+				new Dictionary<string, Func<Process, object>>() {
+					{ "ID",(p)=>p.Id},
+					{ "Name",(p)=>p.ProcessName},
+					{ "Threads",(p)=>p.Threads.Count},
+					{ "Virtual Memory",(p)=>p.VirtualMemorySize64},
+					{ "Working Memory",(p)=>p.WorkingSet64},
+				});
+
+			tableView.RowOffset = ro;
+			tableView.ColumnOffset = co;
+			tableView.EnsureValidScrollOffsets ();
+		}
+	}
+}

+ 49 - 39
UICatalog/Scenarios/TableEditor.cs

@@ -16,6 +16,7 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Top Level Windows")]
 	public class TableEditor : Scenario {
 		TableView tableView;
+		DataTable currentTable;
 		private MenuItem miShowHeaders;
 		private MenuItem miAlwaysShowHeaders;
 		private MenuItem miHeaderOverline;
@@ -139,17 +140,17 @@ namespace UICatalog.Scenarios {
 			// if user clicks the mouse in TableView
 			tableView.MouseClick += (s,e) => {
 
-				tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
+				tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
 
 				if (clickedCol != null) {
 					if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
 
 						// left click in a header
-						SortColumn (clickedCol);
+						SortColumn (clickedCol.Value);
 					} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
 
 						// right click in a header
-						ShowHeaderContextMenu (clickedCol, e);
+						ShowHeaderContextMenu (clickedCol.Value, e);
 					}
 				}
 			};
@@ -165,32 +166,32 @@ namespace UICatalog.Scenarios {
 			tableView.Update ();
 		}
 
-		private void SortColumn (DataColumn clickedCol)
+		private void SortColumn (int clickedCol)
 		{
 			var sort = GetProposedNewSortOrder (clickedCol, out var isAsc);
 
 			SortColumn (clickedCol, sort, isAsc);
 		}
 
-		private void SortColumn (DataColumn clickedCol, string sort, bool isAsc)
+		private void SortColumn (int clickedCol, string sort, bool isAsc)
 		{
 			// set a sort order
-			tableView.Table.DefaultView.Sort = sort;
+			currentTable.DefaultView.Sort = sort;
 
 			// copy the rows from the view
-			var sortedCopy = tableView.Table.DefaultView.ToTable ();
-			tableView.Table.Rows.Clear ();
+			var sortedCopy = currentTable.DefaultView.ToTable ();
+			currentTable.Rows.Clear ();
 			foreach (DataRow r in sortedCopy.Rows) {
-				tableView.Table.ImportRow (r);
+				currentTable.ImportRow (r);
 			}
 
-			foreach (DataColumn col in tableView.Table.Columns) {
+			foreach (DataColumn col in currentTable.Columns) {
 
 				// remove any lingering sort indicator
 				col.ColumnName = TrimArrows (col.ColumnName);
 
 				// add a new one if this the one that is being sorted
-				if (col == clickedCol) {
+				if (col.Ordinal == clickedCol) {
 					col.ColumnName += isAsc ? '▲' : '▼';
 				}
 			}
@@ -206,29 +207,31 @@ namespace UICatalog.Scenarios {
 		{
 			return columnName.Replace ("▼", "").Replace ("▲", "");
 		}
-		private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc)
+		private string GetProposedNewSortOrder (int clickedCol, out bool isAsc)
 		{
 			// work out new sort order
-			var sort = tableView.Table.DefaultView.Sort;
+			var sort = currentTable.DefaultView.Sort;
+			var colName = currentTable.Columns[clickedCol];
 
 			if (sort?.EndsWith ("ASC") ?? false) {
-				sort = $"{clickedCol.ColumnName} DESC";
+				sort = $"{colName} DESC";
 				isAsc = false;
 			} else {
-				sort = $"{clickedCol.ColumnName} ASC";
+				sort = $"{colName} ASC";
 				isAsc = true;
 			}
 
 			return sort;
 		}
 
-		private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e)
+		private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e)
 		{
 			var sort = GetProposedNewSortOrder (clickedCol, out var isAsc);
+			var colName = tableView.Table.ColumnNames[clickedCol];
 
 			var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1,
 				new MenuBarItem (new MenuItem [] {
-					new MenuItem ($"Hide {TrimArrows(clickedCol.ColumnName)}", "", () => HideColumn(clickedCol)),
+					new MenuItem ($"Hide {TrimArrows(colName)}", "", () => HideColumn(clickedCol)),
 					new MenuItem ($"Sort {StripArrows(sort)}","",()=>SortColumn(clickedCol,sort,isAsc)),
 				})
 			);
@@ -236,7 +239,7 @@ namespace UICatalog.Scenarios {
 			contextMenu.Show ();
 		}
 
-		private void HideColumn (DataColumn clickedCol)
+		private void HideColumn (int clickedCol)
 		{
 			var style = tableView.Style.GetOrCreateColumnStyle (clickedCol);
 			style.Visible = false;
@@ -248,16 +251,16 @@ namespace UICatalog.Scenarios {
 			if (tableView.Table == null)
 				return null;
 
-			if (tableView.SelectedColumn < 0 || tableView.SelectedColumn > tableView.Table.Columns.Count)
+			if (tableView.SelectedColumn < 0 || tableView.SelectedColumn > tableView.Table.Columns)
 				return null;
 
-			return tableView.Table.Columns [tableView.SelectedColumn];
+			return currentTable.Columns [tableView.SelectedColumn];
 		}
 
 		private void SetMinAcceptableWidthToOne ()
 		{
-			foreach (DataColumn c in tableView.Table.Columns) {
-				var style = tableView.Style.GetOrCreateColumnStyle (c);
+			foreach (DataColumn c in currentTable.Columns) {
+				var style = tableView.Style.GetOrCreateColumnStyle (c.Ordinal);
 				style.MinAcceptableWidth = 1;
 			}
 		}
@@ -288,7 +291,7 @@ namespace UICatalog.Scenarios {
 			cancel.Clicked += (s,e) => { Application.RequestStop (); };
 			var d = new Dialog (ok, cancel) { Title = prompt };
 
-			var style = tableView.Style.GetOrCreateColumnStyle (col);
+			var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal);
 
 			var lbl = new Label () {
 				X = 0,
@@ -341,7 +344,7 @@ namespace UICatalog.Scenarios {
 			};*/
 
 			tableView.DrawContent += (s,e) => {
-				_scrollBar.Size = tableView.Table?.Rows?.Count ?? 0;
+				_scrollBar.Size = tableView.Table?.Rows ?? 0;
 				_scrollBar.Position = tableView.RowOffset;
 				//	_scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1;
 				//	_scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
@@ -357,12 +360,12 @@ namespace UICatalog.Scenarios {
 				if (tableView.FullRowSelect) {
 					// Delete button deletes all rows when in full row mode
 					foreach (int toRemove in tableView.GetAllSelectedCells ().Select (p => p.Y).Distinct ().OrderByDescending (i => i))
-						tableView.Table.Rows.RemoveAt (toRemove);
+						currentTable.Rows.RemoveAt (toRemove);
 				} else {
 
 					// otherwise set all selected cells to null
 					foreach (var pt in tableView.GetAllSelectedCells ()) {
-						tableView.Table.Rows [pt.Y] [pt.X] = DBNull.Value;
+						currentTable.Rows [pt.Y] [pt.X] = DBNull.Value;
 					}
 				}
 
@@ -520,13 +523,18 @@ namespace UICatalog.Scenarios {
 
 		private void OpenExample (bool big)
 		{
-			tableView.Table = BuildDemoDataTable (big ? 30 : 5, big ? 1000 : 5);
+			SetTable(BuildDemoDataTable (big ? 30 : 5, big ? 1000 : 5));
 			SetDemoTableStyles ();
 		}
 
+		private void SetTable (DataTable dataTable)
+		{
+			tableView.Table = new DataTableSource(currentTable = dataTable);
+		}
+
 		private void OpenUnicodeMap ()
 		{
-			tableView.Table = BuildUnicodeMap ();
+			SetTable(BuildUnicodeMap ());
 			tableView.Update ();
 		}
 
@@ -538,7 +546,7 @@ namespace UICatalog.Scenarios {
 			for (int i = 0; i < 10; i++) {
 
 				var col = dt.Columns.Add (i.ToString (), typeof (uint));
-				var style = tableView.Style.GetOrCreateColumnStyle (col);
+				var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal);
 				style.RepresentationGetter = (o) => new Rune ((uint)o).ToString ();
 			}
 
@@ -546,7 +554,7 @@ namespace UICatalog.Scenarios {
 			for (int i = 'a'; i < 'a' + 26; i++) {
 
 				var col = dt.Columns.Add (((char)i).ToString (), typeof (uint));
-				var style = tableView.Style.GetOrCreateColumnStyle (col);
+				var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal);
 				style.RepresentationGetter = (o) => new Rune ((uint)o).ToString ();
 			}
 
@@ -727,6 +735,8 @@ namespace UICatalog.Scenarios {
 		};
 		private void SetDemoTableStyles ()
 		{
+			tableView.Style.ColumnStyles.Clear();
+
 			var alignMid = new TableView.ColumnStyle () {
 				Alignment = TextAlignment.Centered
 			};
@@ -760,28 +770,28 @@ namespace UICatalog.Scenarios {
 								null
 			};
 
-			tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["DateCol"], dateFormatStyle);
-			tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["DoubleCol"], negativeRight);
-			tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["NullsCol"], alignMid);
-			tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["IntCol"], alignRight);
+			tableView.Style.ColumnStyles.Add (currentTable.Columns ["DateCol"].Ordinal, dateFormatStyle);
+			tableView.Style.ColumnStyles.Add (currentTable.Columns ["DoubleCol"].Ordinal, negativeRight);
+			tableView.Style.ColumnStyles.Add (currentTable.Columns ["NullsCol"].Ordinal, alignMid);
+			tableView.Style.ColumnStyles.Add (currentTable.Columns ["IntCol"].Ordinal, alignRight);
 
 			tableView.Update ();
 		}
 
 		private void OpenSimple (bool big)
 		{
-			tableView.Table = BuildSimpleDataTable (big ? 30 : 5, big ? 1000 : 5);
+			SetTable(BuildSimpleDataTable (big ? 30 : 5, big ? 1000 : 5));
 		}
 
 		private void EditCurrentCell (object sender, CellActivatedEventArgs e)
 		{
 			if (e.Table == null)
 				return;
-			var o = e.Table.Rows [e.Row] [e.Col];
+			var o = currentTable.Rows [e.Row] [e.Col];
 
 			var title = o is uint u ? GetUnicodeCategory (u) + $"(0x{o:X4})" : "Enter new value";
 
-			var oldValue = e.Table.Rows [e.Row] [e.Col].ToString ();
+			var oldValue = currentTable.Rows [e.Row] [e.Col].ToString ();
 			bool okPressed = false;
 
 			var ok = new Button ("Ok", is_default: true);
@@ -793,7 +803,7 @@ namespace UICatalog.Scenarios {
 			var lbl = new Label () {
 				X = 0,
 				Y = 1,
-				Text = e.Table.Columns [e.Col].ColumnName
+				Text = currentTable.Columns [e.Col].ColumnName
 			};
 
 			var tf = new TextField () {
@@ -811,7 +821,7 @@ namespace UICatalog.Scenarios {
 			if (okPressed) {
 
 				try {
-					e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (tf.Text.ToString ()) ? DBNull.Value : (object)tf.Text;
+					currentTable.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (tf.Text.ToString ()) ? DBNull.Value : (object)tf.Text;
 				} catch (Exception ex) {
 					MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok");
 				}

+ 59 - 36
UICatalog/UICatalog.cs

@@ -12,9 +12,10 @@ using System.Reflection;
 using System.Threading;
 using static Terminal.Gui.ConfigurationManager;
 using System.Text.Json.Serialization;
+using static Terminal.Gui.TableView;
 
-#nullable enable
 
+#nullable enable
 /// <summary>
 /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the catalog of scenarios.
 /// </summary>
@@ -253,8 +254,8 @@ namespace UICatalog {
 			public MenuItem? miIsMouseDisabled;
 			public MenuItem? miEnableConsoleScrolling;
 
-			public ListView CategoryListView;
-			public ListView ScenarioListView;
+			public ListView CategoryList;
+			public TableView ScenarioList;
 
 			public StatusItem Capslock;
 			public StatusItem Numlock;
@@ -323,7 +324,7 @@ namespace UICatalog {
 				//ContentPane.SetSplitterPos (0, 25);
 				//ContentPane.ShortcutAction = () => ContentPane.SetFocus ();
 
-				CategoryListView = new ListView (_categories) {
+				CategoryList = new ListView (_categories) {
 					X = 0,
 					Y = 1,
 					Width = Dim.Percent (30),
@@ -334,37 +335,53 @@ namespace UICatalog {
 					BorderStyle = LineStyle.Single,
 					SuperViewRendersLineCanvas = true
 				};
-				CategoryListView.OpenSelectedItem += (s, a) => {
-					ScenarioListView!.SetFocus ();
+				CategoryList.OpenSelectedItem += (s, a) => {
+					ScenarioList!.SetFocus ();
 				};
-				CategoryListView.SelectedItemChanged += CategoryListView_SelectedChanged;
-
-				//ContentPane.Tiles.ElementAt (0).Title = "Categories";
-				//ContentPane.Tiles.ElementAt (0).MinSize = 2;
-				//ContentPane.Tiles.ElementAt (0).ContentView.Add (CategoryListView);
+				CategoryList.SelectedItemChanged += CategoryView_SelectedChanged;
 
-				ScenarioListView = new ListView () {
-					X = Pos.Right (CategoryListView) - 1,
+				ScenarioList = new TableView () {
+					X = Pos.Right (CategoryList) - 1,
 					Y = 1,
 					Width = Dim.Fill (0),
 					Height = Dim.Fill (1),
-					AllowsMarking = false,
+					//AllowsMarking = false,
 					CanFocus = true,
 					Title = "Scenarios",
 					BorderStyle = LineStyle.Single,
 					SuperViewRendersLineCanvas = true
 				};
-
-				ScenarioListView.OpenSelectedItem += ScenarioListView_OpenSelectedItem;
-
-				//ContentPane.Tiles.ElementAt (1).Title = "Scenarios";
-				//ContentPane.Tiles.ElementAt (1).ContentView.Add (ScenarioListView);
-				//ContentPane.Tiles.ElementAt (1).MinSize = 2;
+				ScenarioList.FullRowSelect = true;
+				//ScenarioList.Style.ShowHeaders = false;
+				ScenarioList.Style.ShowHorizontalHeaderOverline = false;
+				//ScenarioList.Style.ShowHorizontalHeaderUnderline = false;
+				ScenarioList.Style.ShowHorizontalBottomline = false;
+				ScenarioList.Style.ShowVerticalCellLines = false;
+				ScenarioList.Style.ShowVerticalHeaderLines = false;
+
+				/* By default TableView lays out columns at render time and only
+				 * measures y rows of data at a time.  Where y is the height of the
+				 * console. This is for the following reasons:
+				 * 
+				 * - Performance, when tables have a large amount of data
+				 * - Defensive, prevents a single wide cell value pushing other
+				 *   columns off screen (requiring horizontal scrolling
+				 * 
+				 * In the case of UICatalog here, such an approach is overkill so
+				 * we just measure all the data ourselves and set the appropriate
+				 * max widths as ColumnStyles 
+				 */
+
+				var longestName = _scenarios!.Max (s => s.GetName ().Length);
+				ScenarioList.Style.ColumnStyles.Add (0, new ColumnStyle () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName });
+				ScenarioList.Style.ColumnStyles.Add (1, new ColumnStyle () {  MaxWidth = 1 });
+
+				ScenarioList.CellActivated += ScenarioView_OpenSelectedItem;
 
 				KeyDown += KeyDownHandler;
-				//Add (ContentPane);
-				Add (CategoryListView);
-				Add (ScenarioListView);
+
+				Add (CategoryList);
+				Add (ScenarioList);
 
 				Add (MenuBar);
 				Add (StatusBar);
@@ -373,8 +390,10 @@ namespace UICatalog {
 				Unloaded += UnloadedHandler;
 
 				// Restore previous selections
-				CategoryListView.SelectedItem = _cachedCategoryIndex;
-				ScenarioListView.SelectedItem = _cachedScenarioIndex;
+				CategoryList.SelectedItem = _cachedCategoryIndex;
+				CategoryList.EnsureSelectedItemVisible ();
+				ScenarioList.SelectedRow = _cachedScenarioIndex;
+				ScenarioList.EnsureSelectedCellIsVisible ();
 
 				ConfigurationManager.Applied += ConfigAppliedHandler;
 			}
@@ -393,15 +412,15 @@ namespace UICatalog {
 					_isFirstRunning = false;
 				}
 				if (!_isFirstRunning) {
-					ScenarioListView.SetFocus ();
+					ScenarioList.SetFocus ();
 				}
 
 				StatusBar.VisibleChanged += (s, e) => {
 					UICatalogApp.ShowStatusBar = StatusBar.Visible;
 
 					var height = (StatusBar.Visible ? 1 : 0);
-					CategoryListView.Height = Dim.Fill (height);
-					ScenarioListView.Height = Dim.Fill (height);
+					CategoryList.Height = Dim.Fill (height);
+					ScenarioList.Height = Dim.Fill (height);
 					// ContentPane.Height = Dim.Fill (height);
 					LayoutSubviews ();
 					SetSubViewNeedsDisplay ();
@@ -425,16 +444,16 @@ namespace UICatalog {
 			/// Launches the selected scenario, setting the global _selectedScenario
 			/// </summary>
 			/// <param name="e"></param>
-			void ScenarioListView_OpenSelectedItem (object? sender, EventArgs? e)
+			void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e)
 			{
 				if (_selectedScenario is null) {
 					// Save selected item state
-					_cachedCategoryIndex = CategoryListView.SelectedItem;
-					_cachedScenarioIndex = ScenarioListView.SelectedItem;
-					// Create new instance of scenario (even though Scenarios contains instances)
-					var sourceList = ScenarioListView.Source.ToList ();
+					_cachedCategoryIndex = CategoryList.SelectedItem;
+					_cachedScenarioIndex = ScenarioList.SelectedRow;
 
-					_selectedScenario = (Scenario)Activator.CreateInstance (ScenarioListView.Source.ToList () [ScenarioListView.SelectedItem]!.GetType ())!;
+					// Create new instance of scenario (even though Scenarios contains instances)
+					string selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0];
+					_selectedScenario = (Scenario)Activator.CreateInstance (_scenarios!.FirstOrDefault (s => s.GetName () == selectedScenarioName)!.GetType ())!;
 
 					// Tell the main app to stop
 					Application.RequestStop ();
@@ -729,18 +748,22 @@ namespace UICatalog {
 				}
 			}
 
-			void CategoryListView_SelectedChanged (object? sender, ListViewItemEventArgs? e)
+			void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e)
 			{
 				var item = _categories! [e!.Item];
 				List<Scenario> newlist;
 				if (e.Item == 0) {
 					// First category is "All"
 					newlist = _scenarios!;
+					newlist = _scenarios!;
 
 				} else {
 					newlist = _scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ();
 				}
-				ScenarioListView.SetSource (newlist.ToList ());
+				ScenarioList.Table = new EnumerableTableSource<Scenario> (newlist, new Dictionary<string, Func<Scenario, object>> () {
+					{ "Name", (s) => s.GetName() },
+					{ "Description", (s) => s.GetDescription() },
+				});
 			}
 		}
 

+ 132 - 88
UnitTests/Views/TableViewTests.cs

@@ -28,7 +28,7 @@ namespace Terminal.Gui.ViewsTests {
 			Assert.Equal (0, tableView.ColumnOffset);
 
 			// Set empty table
-			tableView.Table = new DataTable ();
+			tableView.Table = new DataTableSource(new DataTable ());
 
 			// Since table has no rows or columns scroll offset should default to 0
 			tableView.EnsureValidScrollOffsets ();
@@ -83,10 +83,10 @@ namespace Terminal.Gui.ViewsTests {
 			tableView.Bounds = new Rect (0, 0, 25, 10);
 
 			// Set a table with 1 column
-			tableView.Table = BuildTable (1, 50);
+			tableView.Table = BuildTable (1, 50, out var dt);
 			tableView.Redraw (tableView.Bounds);
 
-			tableView.Table.Columns.Remove (tableView.Table.Columns [0]);
+			dt.Columns.Remove (dt.Columns [0]);
 			tableView.Redraw (tableView.Bounds);
 		}
 
@@ -286,7 +286,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			// create a 4 by 4 table
 			var tableView = new TableView () {
-				Table = BuildTable (4, 4),
+				Table = BuildTable (4, 4, out var dt),
 				MultiSelect = true,
 				Bounds = new Rect (0, 0, 10, 5)
 			};
@@ -296,13 +296,13 @@ namespace Terminal.Gui.ViewsTests {
 			Assert.Equal (16, tableView.GetAllSelectedCells ().Count ());
 
 			// delete one of the columns
-			tableView.Table.Columns.RemoveAt (2);
+			dt.Columns.RemoveAt (2);
 
 			// table should now be 3x4
 			Assert.Equal (12, tableView.GetAllSelectedCells ().Count ());
 
 			// remove a row
-			tableView.Table.Rows.RemoveAt (1);
+			dt.Rows.RemoveAt (1);
 
 			// table should now be 3x3
 			Assert.Equal (9, tableView.GetAllSelectedCells ().Count ());
@@ -313,7 +313,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			// create a 4 by 4 table
 			var tableView = new TableView () {
-				Table = BuildTable (4, 4),
+				Table = BuildTable (4, 4, out var dt),
 				MultiSelect = true,
 				Bounds = new Rect (0, 0, 10, 5)
 			};
@@ -328,7 +328,7 @@ namespace Terminal.Gui.ViewsTests {
 			Assert.Equal (4, tableView.GetAllSelectedCells ().Count ());
 
 			// remove a row
-			tableView.Table.Rows.RemoveAt (0);
+			dt.Rows.RemoveAt (0);
 
 			tableView.EnsureValidSelection ();
 
@@ -598,7 +598,7 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			string activatedValue = null;
 			var tv = new TableView (BuildTable (1, 1));
-			tv.CellActivated += (s, c) => activatedValue = c.Table.Rows [c.Row] [c.Col].ToString ();
+			tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row,c.Col].ToString();
 
 			Application.Top.Add (tv);
 			Application.Begin (Application.Top);
@@ -633,8 +633,8 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TableViewMultiSelect_CannotFallOffLeft ()
 		{
-			var tv = SetUpMiniTable ();
-			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)
+			var tv = SetUpMiniTable (out var dt);
+			dt.Rows.Add (1, 2); // add another row (brings us to 2 rows)
 
 			tv.MultiSelect = true;
 			tv.SelectedColumn = 1;
@@ -656,8 +656,8 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TableViewMultiSelect_CannotFallOffRight ()
 		{
-			var tv = SetUpMiniTable ();
-			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)
+			var tv = SetUpMiniTable (out var dt);
+			dt.Rows.Add (1, 2); // add another row (brings us to 2 rows)
 
 			tv.MultiSelect = true;
 			tv.SelectedColumn = 0;
@@ -679,8 +679,8 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TableViewMultiSelect_CannotFallOffBottom ()
 		{
-			var tv = SetUpMiniTable ();
-			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)
+			var tv = SetUpMiniTable (out var dt);
+			dt.Rows.Add (1, 2); // add another row (brings us to 2 rows)
 
 			tv.MultiSelect = true;
 			tv.SelectedColumn = 0;
@@ -704,8 +704,8 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TableViewMultiSelect_CannotFallOffTop ()
 		{
-			var tv = SetUpMiniTable ();
-			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)
+			var tv = SetUpMiniTable (out var dt);
+			dt.Rows.Add (1, 2); // add another row (brings us to 2 rows)
 			tv.LayoutSubviews ();
 
 			tv.MultiSelect = true;
@@ -764,8 +764,8 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TestControlClick_MultiSelect_ThreeRowTable_FullRowSelect ()
 		{
-			var tv = GetTwoRowSixColumnTable ();
-			tv.Table.Rows.Add (1, 2, 3, 4, 5, 6);
+			var tv = GetTwoRowSixColumnTable (out var dt);
+			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			tv.LayoutSubviews ();
 
 			tv.MultiSelect = true;
@@ -893,7 +893,7 @@ namespace Terminal.Gui.ViewsTests {
 		[InlineData (true)]
 		public void TableView_ColorsTest_RowColorGetter (bool focused)
 		{
-			var tv = SetUpMiniTable ();
+			var tv = SetUpMiniTable (out DataTable dt);
 			tv.LayoutSubviews ();
 
 			// width exactly matches the max col widths
@@ -907,7 +907,7 @@ namespace Terminal.Gui.ViewsTests {
 			};
 
 			// when B is 2 use the custom highlight colour for the row
-			tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table.Rows [e.RowIndex] [1]) == 2 ? rowHighlight : null;
+			tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table[e.RowIndex,1]) == 2 ? rowHighlight : null;
 
 			// private method for forcing the view to be focused/not focused
 			var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic);
@@ -944,7 +944,7 @@ namespace Terminal.Gui.ViewsTests {
 			// it no longer matches the RowColorGetter
 			// delegate conditional ( which checks for
 			// the value 2)
-			tv.Table.Rows [0] [1] = 5;
+			dt.Rows [0][1] = 5;
 
 			tv.Redraw (tv.Bounds);
 			expected = @"
@@ -980,14 +980,14 @@ namespace Terminal.Gui.ViewsTests {
 		[InlineData (true)]
 		public void TableView_ColorsTest_ColorGetter (bool focused)
 		{
-			var tv = SetUpMiniTable ();
+			var tv = SetUpMiniTable (out var dt);
 			tv.LayoutSubviews ();
 
 			// width exactly matches the max col widths
 			tv.Bounds = new Rect (0, 0, 5, 4);
 
 			// Create a style for column B
-			var bStyle = tv.Style.GetOrCreateColumnStyle (tv.Table.Columns ["B"]);
+			var bStyle = tv.Style.GetOrCreateColumnStyle (1);
 
 			// when B is 2 use the custom highlight colour
 			var cellHighlight = new ColorScheme () {
@@ -1034,7 +1034,7 @@ namespace Terminal.Gui.ViewsTests {
 			// it no longer matches the ColorGetter
 			// delegate conditional ( which checks for
 			// the value 2)
-			tv.Table.Rows [0] [1] = 5;
+			dt.Rows [0] [1] = 5;
 
 			tv.Redraw (tv.Bounds);
 			expected = @"
@@ -1066,21 +1066,25 @@ namespace Terminal.Gui.ViewsTests {
 		}
 
 		private TableView SetUpMiniTable ()
+		{
+			return SetUpMiniTable (out _);
+		}
+		private TableView SetUpMiniTable (out DataTable dt)
 		{
 			var tv = new TableView ();
 			tv.BeginInit (); tv.EndInit ();
 			tv.Bounds = new Rect (0, 0, 10, 4);
 
-			var dt = new DataTable ();
-			var colA = dt.Columns.Add ("A");
-			var colB = dt.Columns.Add ("B");
+			dt = new DataTable ();
+			dt.Columns.Add ("A");
+			dt.Columns.Add ("B");
 			dt.Rows.Add (1, 2);
 
-			tv.Table = dt;
-			tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
-			tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
-			tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;
-			tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;
+			tv.Table = new DataTableSource(dt);
+			tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1;
+			tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1;
+			tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1;
+			tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1;
 
 			tv.ColorScheme = Colors.Base;
 			return tv;
@@ -1140,7 +1144,7 @@ namespace Terminal.Gui.ViewsTests {
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
-			tableView.Table = dt;
+			tableView.Table = new DataTableSource(dt);
 
 			// select last visible column
 			tableView.SelectedColumn = 2; // column C
@@ -1201,7 +1205,7 @@ namespace Terminal.Gui.ViewsTests {
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
-			tableView.Table = dt;
+			tableView.Table = new DataTableSource(dt);
 
 			// select last visible column
 			tableView.SelectedColumn = 2; // column C
@@ -1260,7 +1264,7 @@ namespace Terminal.Gui.ViewsTests {
 			dt.Columns.Add ("F");
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
-			tableView.Table = dt;
+			tableView.Table = new DataTableSource(dt);
 
 			return tableView;
 		}
@@ -1268,9 +1272,9 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TestColumnStyle_VisibleFalse_IsNotRendered ()
 		{
-			var tableView = GetABCDEFTableView (out DataTable dt);
+			var tableView = GetABCDEFTableView (out _);
 
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (1).Visible = false;
 			tableView.LayoutSubviews ();
 			tableView.Redraw (tableView.Bounds);
 
@@ -1289,7 +1293,7 @@ namespace Terminal.Gui.ViewsTests {
 
 			tableView.Style.ShowHorizontalScrollIndicators = true;
 			tableView.Style.ShowHorizontalHeaderUnderline = true;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (0).Visible = false;
 
 			tableView.LayoutSubviews ();
 			tableView.Redraw (tableView.Bounds);
@@ -1308,12 +1312,10 @@ namespace Terminal.Gui.ViewsTests {
 		{
 			var tableView = GetABCDEFTableView (out DataTable dt);
 
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["C"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+			for (int i = 0; i < 6; i++) {
+				tableView.Style.GetOrCreateColumnStyle (i).Visible = false;
+			}
+			
 			tableView.LayoutSubviews ();
 
 			// expect nothing to be rendered when all columns are invisible
@@ -1351,9 +1353,9 @@ namespace Terminal.Gui.ViewsTests {
 			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// but if DEF are invisible we shouldn't be showing the indicator
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (3).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (4).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (5).Visible = false;
 
 			expected =
 			       @"
@@ -1386,8 +1388,8 @@ namespace Terminal.Gui.ViewsTests {
 			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// but if E and F are invisible so we shouldn't show right
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (4).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (5).Visible = false;
 
 			expected =
 			       @"
@@ -1398,7 +1400,7 @@ namespace Terminal.Gui.ViewsTests {
 			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// now also A is invisible so we cannot scroll in either direction
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (0).Visible = false;
 
 			expected =
 			       @"
@@ -1414,7 +1416,7 @@ namespace Terminal.Gui.ViewsTests {
 			var tableView = GetABCDEFTableView (out var dt);
 			tableView.LayoutSubviews ();
 
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (1).Visible = false;
 			tableView.SelectedColumn = 0;
 
 			tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight });
@@ -1436,7 +1438,7 @@ namespace Terminal.Gui.ViewsTests {
 			var tableView = GetABCDEFTableView (out var dt);
 			tableView.LayoutSubviews ();
 
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (0).Visible = false;
 			tableView.SelectedColumn = 0;
 
 			Assert.Equal (0, tableView.SelectedColumn);
@@ -1505,9 +1507,9 @@ namespace Terminal.Gui.ViewsTests {
 			tableView.SelectedColumn = 3;
 			Assert.Equal (3, tableView.SelectedColumn);
 
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (3).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (4).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (5).Visible = false;
 
 			// column D is invisible so this method should move to 2 (C)
 			tableView.EnsureValidSelection ();
@@ -1541,7 +1543,7 @@ namespace Terminal.Gui.ViewsTests {
 			Assert.False (tableView.IsSelected (3, 0));
 
 			// if middle column is invisible
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (1).Visible = false;
 
 			// it should not be included in the selection
 			Assert.Equal (2, tableView.GetAllSelectedCells ().Count ());
@@ -1556,11 +1558,11 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TestColumnStyle_VisibleFalse_MultiSelectingStepsOverInvisibleColumns ()
 		{
-			var tableView = GetABCDEFTableView (out var dt);
+			var tableView = GetABCDEFTableView (out _);
 			tableView.LayoutSubviews ();
 
 			// if middle column is invisible
-			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
+			tableView.Style.GetOrCreateColumnStyle (1).Visible = false;
 
 			tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight | Key.ShiftMask });
 
@@ -1764,7 +1766,7 @@ namespace Terminal.Gui.ViewsTests {
 			tableView.Style.SmoothHorizontalScrolling = smooth;
 
 			if (invisibleCol) {
-				tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
+				tableView.Style.GetOrCreateColumnStyle (3).Visible = false;
 			}
 
 			// New TableView should have first cell selected 
@@ -1812,7 +1814,7 @@ namespace Terminal.Gui.ViewsTests {
 			dt.Rows.Add (1, 2, new string ('a', 500));
 			dt.Rows.Add (1, 2, "aaa");
 
-			tableView.Table = dt;
+			tableView.Table = new DataTableSource(dt);
 			tableView.LayoutSubviews ();
 			tableView.Redraw (tableView.Bounds);
 
@@ -1829,7 +1831,7 @@ namespace Terminal.Gui.ViewsTests {
 			TestHelpers.AssertDriverContentsAre (expected, output);
 
 			// get a style for the long column
-			var style = tableView.Style.GetOrCreateColumnStyle (dt.Columns [2]);
+			var style = tableView.Style.GetOrCreateColumnStyle (2);
 
 			// one way the API user can fix this for long columns
 			// is to specify a MinAcceptableWidth for the column
@@ -1955,7 +1957,7 @@ namespace Terminal.Gui.ViewsTests {
 
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
-			tableView.Table = dt;
+			tableView.Table = new DataTableSource(dt);
 
 			// select last visible column
 			tableView.SelectedColumn = 2; // column C
@@ -2020,7 +2022,7 @@ namespace Terminal.Gui.ViewsTests {
 
 			dt.Rows.Add ("Hello", DBNull.Value, "f");
 
-			tv.Table = dt;
+			tv.Table = new DataTableSource(dt);
 			tv.NullSymbol = string.Empty;
 
 			Application.Top.Add (tv);
@@ -2051,8 +2053,9 @@ namespace Terminal.Gui.ViewsTests {
 			// Now the thing we really want to test is the styles!
 			// All cells in the column have a column style that says
 			// the cell is pink!
-			foreach (DataColumn col in dt.Columns) {
-				var style = tv.Style.GetOrCreateColumnStyle (col);
+			for (int i = 0; i < dt.Columns.Count; i++) {
+
+				var style = tv.Style.GetOrCreateColumnStyle (i);
 				style.ColorGetter = (e) => {
 					return scheme;
 				};
@@ -2135,12 +2138,13 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TestFullRowSelect_SelectionColorStopsAtTableEdge_WithCellLines ()
 		{
-			var tv = GetTwoRowSixColumnTable ();
-			tv.Table.Rows.Add (1, 2, 3, 4, 5, 6);
-			tv.LayoutSubviews ();
-
+			var tv = GetTwoRowSixColumnTable (out var dt);
+			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
 			tv.Bounds = new Rect (0, 0, 7, 6);
+			tv.Frame = new Rect (0, 0, 7, 6);
+			tv.LayoutSubviews ();
+
 
 			tv.FullRowSelect = true;
 			tv.Style.ShowHorizontalBottomline = true;
@@ -2192,12 +2196,12 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TestFullRowSelect_AlwaysUseNormalColorForVerticalCellLines ()
 		{
-			var tv = GetTwoRowSixColumnTable ();
-			tv.Table.Rows.Add (1, 2, 3, 4, 5, 6);
-			tv.LayoutSubviews ();
-
+			var tv = GetTwoRowSixColumnTable (out var dt);
+			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
 			tv.Bounds = new Rect (0, 0, 7, 6);
+			tv.Frame = new Rect (0, 0, 7, 6);
+			tv.LayoutSubviews ();
 
 			tv.FullRowSelect = true;
 			tv.Style.ShowHorizontalBottomline = true;
@@ -2248,8 +2252,8 @@ namespace Terminal.Gui.ViewsTests {
 		[Fact, AutoInitShutdown]
 		public void TestFullRowSelect_SelectionColorDoesNotStop_WhenShowVerticalCellLinesIsFalse ()
 		{
-			var tv = GetTwoRowSixColumnTable ();
-			tv.Table.Rows.Add (1, 2, 3, 4, 5, 6);
+			var tv = GetTwoRowSixColumnTable (out var dt);
+			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			tv.LayoutSubviews ();
 
 
@@ -2301,15 +2305,20 @@ A B C
 			TestHelpers.AssertDriverColorsAre (expected, normal, focus);
 		}
 
+		public static DataTableSource BuildTable (int cols, int rows)
+		{
+			return BuildTable (cols, rows, out _);
+		}
+
 		/// <summary>
 		/// Builds a simple table of string columns with the requested number of columns and rows
 		/// </summary>
 		/// <param name="cols"></param>
 		/// <param name="rows"></param>
 		/// <returns></returns>
-		public static DataTable BuildTable (int cols, int rows)
+		public static DataTableSource BuildTable (int cols, int rows, out DataTable dt)
 		{
-			var dt = new DataTable ();
+			dt = new DataTable ();
 
 			for (int c = 0; c < cols; c++) {
 				dt.Columns.Add ("Col" + c);
@@ -2325,7 +2334,7 @@ A B C
 				dt.Rows.Add (newRow);
 			}
 
-			return dt;
+			return new DataTableSource(dt);
 		}
 
 		[Fact, AutoInitShutdown]
@@ -2412,7 +2421,7 @@ A B C
 │1│2│3│";
 
 			TestHelpers.AssertDriverContentsAre (expected, output);
-			DataColumn col;
+			int? col;
 
 			// ---------------- X=0 -----------------------
 			// click is before first cell
@@ -2430,10 +2439,10 @@ A B C
 			// ---------------- X=1 -----------------------
 			// click in header
 			Assert.Null (tableView.ScreenToCell (1, 0, out col));
-			Assert.Equal ("A", col.ColumnName);
+			Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]);
 			// click in header row line  (click in the horizontal line below header counts as click in header above - consistent with the column hit box)
 			Assert.Null (tableView.ScreenToCell (1, 1, out col));
-			Assert.Equal ("A", col.ColumnName);
+			Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]);
 			// click in cell 0,0
 			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (1, 2, out col));
 			Assert.Null (col);
@@ -2447,10 +2456,10 @@ A B C
 			// ---------------- X=2 -----------------------
 			// click in header
 			Assert.Null (tableView.ScreenToCell (2, 0, out col));
-			Assert.Equal ("A", col.ColumnName);
+			Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]);
 			// click in header row line
 			Assert.Null (tableView.ScreenToCell (2, 1, out col));
-			Assert.Equal ("A", col.ColumnName);
+			Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]);
 			// click in cell 0,0
 			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (2, 2, out col));
 			Assert.Null (col);
@@ -2464,10 +2473,10 @@ A B C
 			// ---------------- X=3 -----------------------
 			// click in header
 			Assert.Null (tableView.ScreenToCell (3, 0, out col));
-			Assert.Equal ("B", col.ColumnName);
+			Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]);
 			// click in header row line
 			Assert.Null (tableView.ScreenToCell (3, 1, out col));
-			Assert.Equal ("B", col.ColumnName);
+			Assert.Equal ("B", tableView.Table.ColumnNames[col.Value]);
 			// click in cell 1,0
 			Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col));
 			Assert.Null (col);
@@ -2478,7 +2487,42 @@ A B C
 			Assert.Null (tableView.ScreenToCell (3, 4, out col));
 			Assert.Null (col);
 		}
+
+		[Fact,AutoInitShutdown]
+		public void TestEnumerableDataSource_BasicTypes()
+		{
+			var tv = new TableView ();
+			tv.ColorScheme = Colors.TopLevel;
+			tv.Bounds = new Rect (0, 0, 50, 6);
+
+			tv.Table = new EnumerableTableSource<Type> (
+				new Type [] { typeof (string), typeof (int), typeof (float) },
+				new () {
+					{ "Name", (t)=>t.Name},
+					{ "Namespace", (t)=>t.Namespace},
+					{ "BaseType", (t)=>t.BaseType}
+				});
+
+			tv.LayoutSubviews ();
+
+			tv.Redraw (tv.Bounds);
+
+			string expected =
+				@"
+┌──────┬─────────┬───────────────────────────────┐
+│Name  │Namespace│BaseType                       │
+├──────┼─────────┼───────────────────────────────┤
+│String│System   │System.Object                  │
+│Int32 │System   │System.ValueType               │
+│Single│System   │System.ValueType               │";
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
 		private TableView GetTwoRowSixColumnTable ()
+		{
+			return GetTwoRowSixColumnTable (out _);
+		}
+		private TableView GetTwoRowSixColumnTable (out DataTable dt)
 		{
 			var tableView = new TableView ();
 			tableView.ColorScheme = Colors.TopLevel;
@@ -2490,7 +2534,7 @@ A B C
 			tableView.Style.AlwaysShowHeaders = true;
 			tableView.Style.SmoothHorizontalScrolling = true;
 
-			var dt = new DataTable ();
+			dt = new DataTable ();
 			dt.Columns.Add ("A");
 			dt.Columns.Add ("B");
 			dt.Columns.Add ("C");
@@ -2501,7 +2545,7 @@ A B C
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 			dt.Rows.Add (1, 2, 3, 4, 5, 6);
 
-			tableView.Table = dt;
+			tableView.Table = new DataTableSource(dt);
 			return tableView;
 		}
 	}

+ 21 - 4
docfx/articles/tableview.md

@@ -52,13 +52,30 @@ tableView = new TableView () {
     Height = 10,
 };
 
-tableView.Table = yourDataTable;
+tableView.Table = new DataTableSource(yourDataTable);
+```
+
+## Object data
+If your data objects are not stored in a `System.Data.DataTable` then you can instead
+create a table using `EnumerableTableSource<T>` or implementing your own `ITableSource`
+class.
+
+For example to render data for the currently running processes:
+
+```csharp
+tableView.Table = new EnumerableTableDataSource<Process> (Process.GetProcesses (),
+				new Dictionary<string, Func<Process, object>>() {
+					{ "ID",(p)=>p.Id},
+					{ "Name",(p)=>p.ProcessName},
+					{ "Threads",(p)=>p.Threads.Count},
+					{ "Virtual Memory",(p)=>p.VirtualMemorySize64},
+					{ "Working Memory",(p)=>p.WorkingSet64},
+				});
 ```
 
 ## Table Rendering
-TableView supports any size of table (limited only by the RAM requirements of `System.DataTable`). You can have
-thousands of columns and/or millions of rows if you want. Horizontal and vertical scrolling can be done using
-the mouse or keyboard.
+TableView supports any size of table. You can have thousands of columns and/or millions of rows if you want.
+Horizontal and vertical scrolling can be done using the mouse or keyboard.
 
 TableView uses `ColumnOffset` and `RowOffset` to determine the first visible cell of the `System.DataTable`.
 Rendering then continues until the avaialble console space is exhausted. Updating the `ColumnOffset` and