Skip to content

Commit 65529ab

Browse files
tznindtig
authored andcommitted
Fixes gui-cs#2575 - TableView to use interface instead of System.Data.DataTable (gui-cs#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]>
1 parent 381a631 commit 65529ab

File tree

14 files changed

+4715
-4208
lines changed

14 files changed

+4715
-4208
lines changed

Terminal.Gui/Views/FileDialog.cs

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public partial class FileDialog : Dialog {
107107
private MenuBar allowedTypeMenuBar;
108108
private MenuBarItem allowedTypeMenu;
109109
private MenuItem [] allowedTypeMenuItems;
110-
private DataColumn filenameColumn;
110+
private int filenameColumn;
111111

112112
/// <summary>
113113
/// Event fired when user attempts to confirm a selection (or multi selection).
@@ -226,7 +226,7 @@ public FileDialog (IFileSystem fileSystem)
226226
if (this.tableView.SelectedRow <= 0) {
227227
this.NavigateIf (k, Key.CursorUp, this.tbPath);
228228
}
229-
if (this.tableView.SelectedRow == this.tableView.Table.Rows.Count-1) {
229+
if (this.tableView.SelectedRow == this.tableView.Table.Rows - 1) {
230230
this.NavigateIf (k, Key.CursorDown, this.btnToggleSplitterCollapse);
231231
}
232232

@@ -318,7 +318,7 @@ public FileDialog (IFileSystem fileSystem)
318318
this.sorter = new FileDialogSorter (this, this.tableView);
319319
this.history = new FileDialogHistory (this);
320320

321-
this.tableView.Table = this.dtFiles;
321+
this.tableView.Table = new DataTableSource(this.dtFiles);
322322

323323
this.tbPath.TextChanged += (s, e) => this.PathChanged ();
324324

@@ -514,7 +514,7 @@ private void ClearFeedback ()
514514

515515
private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent)
516516
{
517-
if (tableView.Table.Rows.Count == 0) {
517+
if (tableView.Table.Rows == 0) {
518518
return;
519519
}
520520

@@ -539,11 +539,10 @@ private void UpdateCollectionNavigator ()
539539
{
540540
tableView.EnsureValidSelection ();
541541
var col = tableView.SelectedColumn;
542-
var style = tableView.Style.GetColumnStyleIfAny (tableView.Table.Columns [col]);
542+
var style = tableView.Style.GetColumnStyleIfAny (col);
543543

544544

545-
var collection = tableView
546-
.Table
545+
var collection = dtFiles
547546
.Rows
548547
.Cast<DataRow> ()
549548
.Select ((o, idx) => col == 0 ?
@@ -897,7 +896,7 @@ private void UpdateNavigationVisibility ()
897896

898897
private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEventArgs obj)
899898
{
900-
if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows.Count == 0) {
899+
if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows == 0) {
901900
return;
902901
}
903902

@@ -969,7 +968,7 @@ private void SetupTableColumns ()
969968
this.dtFiles = new DataTable ();
970969

971970
var nameStyle = this.tableView.Style.GetOrCreateColumnStyle (
972-
filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int))
971+
filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int)).Ordinal
973972
);
974973
nameStyle.RepresentationGetter = (i) => {
975974

@@ -989,11 +988,13 @@ private void SetupTableColumns ()
989988

990989
nameStyle.MinWidth = 50;
991990

992-
var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)));
991+
var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (
992+
this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)).Ordinal);
993993
sizeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].HumanReadableLength ?? string.Empty;
994994
nameStyle.MinWidth = 10;
995995

996-
var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)));
996+
var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (
997+
this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)).Ordinal);
997998
dateModifiedStyle.RepresentationGetter = (i) =>
998999
{
9991000
var s = this.State?.Children [(int)i];
@@ -1006,7 +1007,8 @@ private void SetupTableColumns ()
10061007

10071008
dateModifiedStyle.MinWidth = 30;
10081009

1009-
var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)));
1010+
var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (
1011+
this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)).Ordinal);
10101012
typeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].Type ?? string.Empty;
10111013
typeStyle.MinWidth = 6;
10121014

@@ -1237,7 +1239,7 @@ private void WriteStateToTableView ()
12371239

12381240
private void BuildRow (int idx)
12391241
{
1240-
this.tableView.Table.Rows.Add (idx, idx, idx, idx);
1242+
dtFiles.Rows.Add (idx, idx, idx, idx);
12411243
}
12421244

12431245
private ColorScheme ColorGetter (TableView.CellColorGetterArgs args)
@@ -1277,7 +1279,7 @@ private IEnumerable<FileSystemInfoStats> MultiRowToStats ()
12771279

12781280
foreach (var p in this.tableView.GetAllSelectedCells ()) {
12791281

1280-
var add = this.State?.Children [(int)this.tableView.Table.Rows [p.Y] [0]];
1282+
var add = this.State?.Children [(int)this.tableView.Table[p.Y, 0]];
12811283
if (add != null) {
12821284
toReturn.Add (add);
12831285
}
@@ -1288,7 +1290,7 @@ private IEnumerable<FileSystemInfoStats> MultiRowToStats ()
12881290
}
12891291
private FileSystemInfoStats RowToStats (int rowIndex)
12901292
{
1291-
return this.State?.Children [(int)this.tableView.Table.Rows [rowIndex] [0]];
1293+
return this.State?.Children [(int)this.tableView.Table[rowIndex,0]];
12921294
}
12931295
private int? StatsToRow (IFileSystemInfo fileSystemInfo)
12941296
{
@@ -1299,7 +1301,7 @@ private FileSystemInfoStats RowToStats (int rowIndex)
12991301

13001302
// find the row number in our DataTable where the cell
13011303
// contains idx
1302-
var match = tableView.Table.Rows
1304+
var match = dtFiles.Rows
13031305
.Cast<DataRow> ()
13041306
.Select ((r, rIdx) => new { row = r, rowIdx = rIdx })
13051307
.Where (t => (int)t.row [0] == idx)
@@ -1367,7 +1369,7 @@ private class FileDialogSorter {
13671369
private readonly FileDialog dlg;
13681370
private TableView tableView;
13691371

1370-
private DataColumn currentSort = null;
1372+
private int? currentSort = null;
13711373
private bool currentSortIsAsc = true;
13721374

13731375
public FileDialogSorter (FileDialog dlg, TableView tableView)
@@ -1378,17 +1380,17 @@ public FileDialogSorter (FileDialog dlg, TableView tableView)
13781380
// if user clicks the mouse in TableView
13791381
this.tableView.MouseClick += (s, e) => {
13801382

1381-
var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
1383+
var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
13821384

13831385
if (clickedCol != null) {
13841386
if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
13851387

13861388
// left click in a header
1387-
this.SortColumn (clickedCol);
1389+
this.SortColumn (clickedCol.Value);
13881390
} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
13891391

13901392
// right click in a header
1391-
this.ShowHeaderContextMenu (clickedCol, e);
1393+
this.ShowHeaderContextMenu (clickedCol.Value, e);
13921394
}
13931395
} else {
13941396
if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
@@ -1406,10 +1408,14 @@ internal void ApplySort ()
14061408
{
14071409
var col = this.currentSort;
14081410

1411+
if(col == null) {
1412+
return;
1413+
}
1414+
14091415
// TODO: Consider preserving selection
1410-
this.tableView.Table.Rows.Clear ();
1416+
dlg.dtFiles.Rows.Clear ();
14111417

1412-
var colName = col == null ? null : StripArrows (col.ColumnName);
1418+
var colName = col == null ? null : StripArrows (tableView.Table.ColumnNames[col.Value]);
14131419

14141420
var stats = this.dlg.State?.Children ?? new FileSystemInfoStats [0];
14151421

@@ -1438,13 +1444,13 @@ internal void ApplySort ()
14381444
this.dlg.BuildRow (o.i);
14391445
}
14401446

1441-
foreach (DataColumn c in this.tableView.Table.Columns) {
1447+
foreach (DataColumn c in dlg.dtFiles.Columns) {
14421448

14431449
// remove any lingering sort indicator
14441450
c.ColumnName = StripArrows (c.ColumnName);
14451451

14461452
// add a new one if this the one that is being sorted
1447-
if (c == col) {
1453+
if (c.Ordinal == col) {
14481454
c.ColumnName += this.currentSortIsAsc ? " (▲)" : " (▼)";
14491455
}
14501456
}
@@ -1458,13 +1464,13 @@ private static string StripArrows (string columnName)
14581464
return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
14591465
}
14601466

1461-
private void SortColumn (DataColumn clickedCol)
1467+
private void SortColumn (int clickedCol)
14621468
{
14631469
this.GetProposedNewSortOrder (clickedCol, out var isAsc);
14641470
this.SortColumn (clickedCol, isAsc);
14651471
}
14661472

1467-
internal void SortColumn (DataColumn col, bool isAsc)
1473+
internal void SortColumn (int col, bool isAsc)
14681474
{
14691475
// set a sort order
14701476
this.currentSort = col;
@@ -1473,19 +1479,19 @@ internal void SortColumn (DataColumn col, bool isAsc)
14731479
this.ApplySort ();
14741480
}
14751481

1476-
private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc)
1482+
private string GetProposedNewSortOrder (int clickedCol, out bool isAsc)
14771483
{
14781484
// work out new sort order
14791485
if (this.currentSort == clickedCol && this.currentSortIsAsc) {
14801486
isAsc = false;
1481-
return $"{clickedCol.ColumnName} DESC";
1487+
return $"{tableView.Table.ColumnNames[clickedCol]} DESC";
14821488
} else {
14831489
isAsc = true;
1484-
return $"{clickedCol.ColumnName} ASC";
1490+
return $"{tableView.Table.ColumnNames [clickedCol]} ASC";
14851491
}
14861492
}
14871493

1488-
private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e)
1494+
private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e)
14891495
{
14901496
var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc);
14911497

@@ -1494,7 +1500,7 @@ private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e
14941500
e.MouseEvent.Y + 1,
14951501
new MenuBarItem (new MenuItem []
14961502
{
1497-
new MenuItem($"Hide {StripArrows(clickedCol.ColumnName)}", string.Empty, () => this.HideColumn(clickedCol)),
1503+
new MenuItem($"Hide {StripArrows(tableView.Table.ColumnNames[clickedCol])}", string.Empty, () => this.HideColumn(clickedCol)),
14981504
new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)),
14991505
})
15001506
);
@@ -1524,7 +1530,7 @@ private void ShowCellContextMenu (Point? clickedCell, MouseEventEventArgs e)
15241530
contextMenu.Show ();
15251531
}
15261532

1527-
private void HideColumn (DataColumn clickedCol)
1533+
private void HideColumn (int clickedCol)
15281534
{
15291535
var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol);
15301536
style.Visible = false;

Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class CellActivatedEventArgs : EventArgs {
1111
/// 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
1212
/// </summary>
1313
/// <value></value>
14-
public DataTable Table { get; }
14+
public ITableSource Table { get; }
1515

1616
/// <summary>
1717
/// The column index of the <see cref="Table"/> cell that is being activated
@@ -31,7 +31,7 @@ public class CellActivatedEventArgs : EventArgs {
3131
/// <param name="t"></param>
3232
/// <param name="col"></param>
3333
/// <param name="row"></param>
34-
public CellActivatedEventArgs (DataTable t, int col, int row)
34+
public CellActivatedEventArgs (ITableSource t, int col, int row)
3535
{
3636
Table = t;
3737
Col = col;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Data;
2+
using System.Linq;
3+
4+
namespace Terminal.Gui {
5+
/// <summary>
6+
/// <see cref="ITableSource"/> implementation that wraps
7+
/// a <see cref="System.Data.DataTable"/>. This class is
8+
/// mutable: changes are permitted to the wrapped <see cref="DataTable"/>.
9+
/// </summary>
10+
public class DataTableSource : ITableSource
11+
{
12+
private readonly DataTable table;
13+
14+
/// <summary>
15+
/// Creates a new instance based on the data in <paramref name="table"/>.
16+
/// </summary>
17+
/// <param name="table"></param>
18+
public DataTableSource(DataTable table)
19+
{
20+
this.table = table;
21+
}
22+
23+
/// <inheritdoc/>
24+
public object this [int row, int col] => table.Rows[row][col];
25+
26+
/// <inheritdoc/>
27+
public int Rows => table.Rows.Count;
28+
29+
/// <inheritdoc/>
30+
public int Columns => table.Columns.Count;
31+
32+
/// <inheritdoc/>
33+
public string [] ColumnNames => table.Columns.Cast<DataColumn>().Select (c => c.ColumnName).ToArray ();
34+
}
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Terminal.Gui {
6+
7+
/// <summary>
8+
/// <see cref="ITableSource"/> implementation that wraps arbitrary data.
9+
/// </summary>
10+
/// <typeparam name="T"></typeparam>
11+
public class EnumerableTableSource<T> : ITableSource {
12+
private T [] data;
13+
private string [] cols;
14+
private Dictionary<string, Func<T, object>> lamdas;
15+
16+
/// <summary>
17+
/// Creates a new instance of the class that presents <paramref name="data"/>
18+
/// collection as a table.
19+
/// </summary>
20+
/// <remarks>The elements of the <paramref name="data"/> collection are recorded during
21+
/// construction (immutable) but the properties of those objects are permitted to
22+
/// change.</remarks>
23+
/// <param name="data">The data that you want to present. The members of this collection
24+
/// will be frozen after construction.</param>
25+
/// <param name="columnDefinitions">
26+
/// Getter methods for each property you want to present in the table. For example:
27+
/// <code>
28+
/// new () {
29+
/// { "Colname1", (t)=>t.SomeField},
30+
/// { "Colname2", (t)=>t.SomeOtherField}
31+
///}
32+
/// </code></param>
33+
public EnumerableTableSource (IEnumerable<T> data, Dictionary<string, Func<T, object>> columnDefinitions)
34+
{
35+
this.data = data.ToArray ();
36+
this.cols = columnDefinitions.Keys.ToArray ();
37+
this.lamdas = columnDefinitions;
38+
}
39+
40+
/// <inheritdoc/>
41+
public object this [int row, int col] {
42+
get => this.lamdas [ColumnNames [col]] (this.data [row]);
43+
}
44+
45+
/// <inheritdoc/>
46+
public int Rows => data.Length;
47+
48+
/// <inheritdoc/>
49+
public int Columns => cols.Length;
50+
51+
/// <inheritdoc/>
52+
public string [] ColumnNames => cols;
53+
}
54+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace Terminal.Gui {
2+
/// <summary>
3+
/// Tabular matrix of data to be displayed in a <see cref="TableView"/>.
4+
/// </summary>
5+
public interface ITableSource
6+
{
7+
/// <summary>
8+
/// Gets the number of rows in the table.
9+
/// </summary>
10+
int Rows { get; }
11+
12+
/// <summary>
13+
/// Gets the number of columns in the table.
14+
/// </summary>
15+
int Columns { get; }
16+
17+
/// <summary>
18+
/// Gets the label for each column.
19+
/// </summary>
20+
string[] ColumnNames { get; }
21+
22+
/// <summary>
23+
/// Returns the data at the given indexes of the table (row, column).
24+
/// </summary>
25+
/// <param name="row"></param>
26+
/// <param name="col"></param>
27+
/// <returns></returns>
28+
object this[int row, int col]
29+
{
30+
get;
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)