From 5d1fe43362254802180f3e316f392025ee7792fd Mon Sep 17 00:00:00 2001 From: Nutzzz Date: Tue, 9 May 2023 21:03:47 -0700 Subject: [PATCH] Fixes #16 - Add ListTableSource for columned lists (#2603) * Add ListTableSource for TableView to display an IList in a columned view Also adds a global MinCellWidth to TableView * Code style fixes * Include padding in MinCellWidth calculation * Unit Tests for Min/MaxCellWidth and ListTableSource * Rename Redraw to Draw after refactor * Rename Redraw to Draw after refactor --------- Co-authored-by: Tig --- .../Views/TableView/ListTableSource.cs | 199 +++++++++++ Terminal.Gui/Views/TableView/TableView.cs | 17 +- UICatalog/Scenarios/ListColumns.cs | 334 ++++++++++++++++++ UnitTests/Views/TableViewTests.cs | 127 +++++++ 4 files changed, 675 insertions(+), 2 deletions(-) create mode 100644 Terminal.Gui/Views/TableView/ListTableSource.cs create mode 100644 UICatalog/Scenarios/ListColumns.cs diff --git a/Terminal.Gui/Views/TableView/ListTableSource.cs b/Terminal.Gui/Views/TableView/ListTableSource.cs new file mode 100644 index 000000000..265a396af --- /dev/null +++ b/Terminal.Gui/Views/TableView/ListTableSource.cs @@ -0,0 +1,199 @@ +using NStack; +using System; +using System.Collections; +using System.Data; +using System.Linq; + +namespace Terminal.Gui { + /// + /// implementation that wraps + /// a . This class is + /// mutable: changes are permitted to the wrapped . + /// + public class ListTableSource : ITableSource { + /// + /// The list this source wraps. + /// + public IList List; + + /// + /// The style this source uses. + /// + public ListColumnStyle Style; + + /// + /// The data table this source wraps. + /// + public DataTable DataTable { get; private set; } + + private TableView _tableView; + + private Rect _lastBounds; + private int _lastMaxCellWidth; + private int _lastMinCellWidth; + private ListColumnStyle _lastStyle; + private IList _lastList; + + /// + /// Creates a new columned list table instance based on the data in + /// and dimensions from . + /// + /// + /// + /// + public ListTableSource (IList list, TableView tableView, ListColumnStyle style) + { + this.List = list; + this._tableView = tableView; + Style = style; + + this.DataTable = CreateTable (CalculateColumns ()); + + // TODO: Determine the best event for this + tableView.DrawContent += TableView_DrawContent; + } + + /// + public ListTableSource (IList list, TableView tableView) : this (list, tableView, new ListColumnStyle ()) { } + + private void TableView_DrawContent (object sender, DrawEventArgs e) + { + if ((!_tableView.Bounds.Equals (_lastBounds)) || + _tableView.MaxCellWidth != _lastMaxCellWidth || + _tableView.MinCellWidth != _lastMinCellWidth || + Style != _lastStyle || + this.List != _lastList) { + + this.DataTable = CreateTable (CalculateColumns ()); + } + _lastBounds = _tableView.Bounds; + _lastMinCellWidth = _tableView.MaxCellWidth; + _lastMaxCellWidth = _tableView.MaxCellWidth; + _lastStyle = Style; + _lastList = this.List; + } + + /// + public object this [int row, int col] { + get { + int idx; + if (Style.Orientation == Orientation.Vertical) { + idx = (col * Rows) + row; + } else { + idx = (row * Columns) + col; + } + if (idx < 0 || idx >= Count) { + return null; + } + return this.List [idx]; + } + } + + /// + /// The number of items in the IList source + /// + public int Count => this.List.Count; + + /// + public int Rows => this.DataTable.Rows.Count; + + /// + public int Columns => this.DataTable.Columns.Count; + + /// + public string [] ColumnNames => Enumerable.Range (0, Columns).Select (n => n.ToString ()).ToArray (); + + /// + /// Creates a DataTable from an IList to display in a + /// + private DataTable CreateTable (int cols = 1) + { + var table = new DataTable (); + for (int col = 0; col < cols; col++) { + table.Columns.Add (new DataColumn (col.ToString ())); + } + for (int row = 0; row < (Count / table.Columns.Count); row++) { + table.Rows.Add (); + } + // return partial row + if (Count % table.Columns.Count != 0) { + table.Rows.Add (); + } + + return table; + } + + /// + /// Returns the size in characters of the longest value read from + /// + /// + private int CalculateMaxLength () + { + if (List == null || Count == 0) { + return 0; + } + + int maxLength = 0; + foreach (var t in List) { + int l; + if (t is ustring u) { + l = TextFormatter.GetTextWidth (u); + } else if (t is string s) { + l = s.Length; + } else { + l = t.ToString ().Length; + } + + if (l > maxLength) { + maxLength = l; + } + } + + return maxLength; + } + + private int CalculateColumns () + { + int cols; + + int colWidth = CalculateMaxLength (); + if (colWidth > _tableView.MaxCellWidth) { + colWidth = _tableView.MaxCellWidth; + } + + if (_tableView.MinCellWidth > 0 && colWidth < _tableView.MinCellWidth) { + if (_tableView.MinCellWidth > _tableView.MaxCellWidth) { + colWidth = _tableView.MaxCellWidth; + } else { + colWidth = _tableView.MinCellWidth; + } + } + if ((Style.Orientation == Orientation.Vertical) != Style.ScrollParallel) { + float f = (float)_tableView.Bounds.Height - _tableView.GetHeaderHeight (); + cols = (int)Math.Ceiling (Count / f); + } else { + cols = ((int)Math.Ceiling (((float)_tableView.Bounds.Width - 1) / colWidth)) - 2; + } + + return (cols > 1) ? cols : 1; + } + + /// + /// Defines rendering options that affect how the view is displayed. + /// + public class ListColumnStyle { + + /// + /// Gets or sets an Orientation enum indicating whether to populate data down each column + /// rather than across each row. Defaults to . + /// + public Orientation Orientation { get; set; } = Orientation.Horizontal; + + /// + /// Gets or sets a flag indicating whether to scroll in the same direction as . + /// Defaults to . + /// + public bool ScrollParallel { get; set; } = false; + } + } +} diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 66f812f4f..84c592b1a 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -121,6 +121,11 @@ namespace Terminal.Gui { } } + /// + /// The minimum number of characters to render in any given column. + /// + public int MinCellWidth { get; set; } + /// /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others /// @@ -329,7 +334,7 @@ namespace Terminal.Gui { /// Returns the amount of vertical space currently occupied by the header or 0 if it is not visible. /// /// - private int GetHeaderHeightIfAny () + internal int GetHeaderHeightIfAny () { return ShouldRenderHeaders () ? GetHeaderHeight () : 0; } @@ -338,7 +343,7 @@ namespace Terminal.Gui { /// Returns the amount of vertical space required to display the header /// /// - private int GetHeaderHeight () + internal int GetHeaderHeight () { int heightRequired = Style.ShowHeaders ? 1 : 0; @@ -1619,6 +1624,14 @@ namespace Terminal.Gui { // is there enough space for this column (and it's data)? colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding; + if (MinCellWidth > 0 && colWidth < (MinCellWidth + padding)) { + if (MinCellWidth > MaxCellWidth) { + colWidth = MaxCellWidth + padding; + } else { + colWidth = MinCellWidth + padding; + } + } + // there is not enough space for this columns // visible content if (usedSpace + colWidth > availableHorizontalSpace) { diff --git a/UICatalog/Scenarios/ListColumns.cs b/UICatalog/Scenarios/ListColumns.cs new file mode 100644 index 000000000..61e3a629e --- /dev/null +++ b/UICatalog/Scenarios/ListColumns.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using Terminal.Gui; +using static Terminal.Gui.TableView; + +namespace UICatalog.Scenarios { + + [ScenarioMetadata (Name: "ListColumns", Description: "Implements a columned list via a data table.")] + [ScenarioCategory ("TableView")] + [ScenarioCategory ("Controls")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("Text and Formatting")] + [ScenarioCategory ("Top Level Windows")] + public class ListColumns : Scenario { + TableView listColView; + DataTable currentTable; + private MenuItem _miCellLines; + private MenuItem _miExpandLastColumn; + private MenuItem _miAlwaysUseNormalColorForVerticalCellLines; + private MenuItem _miSmoothScrolling; + private MenuItem _miAlternatingColors; + private MenuItem _miCursor; + private MenuItem _miTopline; + private MenuItem _miBottomline; + private MenuItem _miOrientVertical; + private MenuItem _miScrollParallel; + + ColorScheme alternatingColorScheme; + + public override void Setup () + { + Win.Title = this.GetName (); + Win.Y = 1; // menu + Win.Height = Dim.Fill (1); // status bar + Application.Top.LayoutSubviews (); + + this.listColView = new TableView () { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (1), + Style = new TableStyle { + ShowHeaders = false, + ShowHorizontalHeaderOverline = false, + ShowHorizontalHeaderUnderline = false, + ShowHorizontalBottomline = false, + ExpandLastColumn = false, + } + }; + var listColStyle = new ListTableSource.ListColumnStyle (); + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("Open_BigListExample", "", () => OpenSimpleList (true)), + new MenuItem ("Open_SmListExample", "", () => OpenSimpleList (false)), + new MenuItem ("_CloseExample", "", () => CloseExample ()), + new MenuItem ("_Quit", "", () => Quit()), + }), + new MenuBarItem ("_View", new MenuItem [] { + _miTopline = new MenuItem ("_TopLine", "", () => ToggleTopline ()) { Checked = listColView.Style.ShowHorizontalHeaderOverline, CheckType = MenuItemCheckStyle.Checked }, + _miBottomline = new MenuItem ("_BottomLine", "", () => ToggleBottomline ()) { Checked = listColView.Style.ShowHorizontalBottomline, CheckType = MenuItemCheckStyle.Checked }, + _miCellLines = new MenuItem ("_CellLines", "", () => ToggleCellLines ()) { Checked = listColView.Style.ShowVerticalCellLines, CheckType = MenuItemCheckStyle.Checked }, + _miExpandLastColumn = new MenuItem ("_ExpandLastColumn", "", () => ToggleExpandLastColumn ()) { Checked = listColView.Style.ExpandLastColumn, CheckType = MenuItemCheckStyle.Checked }, + _miAlwaysUseNormalColorForVerticalCellLines = new MenuItem ("_AlwaysUseNormalColorForVerticalCellLines", "", () => ToggleAlwaysUseNormalColorForVerticalCellLines ()) { Checked = listColView.Style.AlwaysUseNormalColorForVerticalCellLines, CheckType = MenuItemCheckStyle.Checked }, + _miSmoothScrolling = new MenuItem ("_SmoothHorizontalScrolling", "", () => ToggleSmoothScrolling ()) { Checked = listColView.Style.SmoothHorizontalScrolling, CheckType = MenuItemCheckStyle.Checked }, + _miAlternatingColors = new MenuItem ("Alternating Colors", "", () => ToggleAlternatingColors ()) { CheckType = MenuItemCheckStyle.Checked}, + _miCursor = new MenuItem ("Invert Selected Cell First Character", "", () => ToggleInvertSelectedCellFirstCharacter ()) { Checked = listColView.Style.InvertSelectedCellFirstCharacter,CheckType = MenuItemCheckStyle.Checked}, + }), + new MenuBarItem ("_List", new MenuItem [] { + //new MenuItem ("_Hide Headers", "", HideHeaders), + _miOrientVertical = new MenuItem ("_OrientVertical", "", () => ToggleVerticalOrientation ()) { Checked = listColStyle.Orientation == Orientation.Vertical, CheckType = MenuItemCheckStyle.Checked }, + _miScrollParallel = new MenuItem ("_ScrollParallel", "", () => ToggleScrollParallel ()) { Checked = listColStyle.ScrollParallel, CheckType = MenuItemCheckStyle.Checked }, + new MenuItem ("Set _Max Cell Width", "", SetListMaxWidth), + new MenuItem ("Set Mi_n Cell Width", "", SetListMinWidth), + }), + }); + + Application.Top.Add (menu); + + var statusBar = new StatusBar (new StatusItem [] { + new StatusItem(Key.F2, "~F2~ OpenBigListEx", () => OpenSimpleList (true)), + new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample ()), + new StatusItem(Key.F4, "~F4~ OpenSmListEx", () => OpenSimpleList (false)), + new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), + }); + Application.Top.Add (statusBar); + + Win.Add (listColView); + + var selectedCellLabel = new Label () { + X = 0, + Y = Pos.Bottom (listColView), + Text = "0,0", + Width = Dim.Fill (), + TextAlignment = TextAlignment.Right + + }; + + Win.Add (selectedCellLabel); + + listColView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{listColView.SelectedRow},{listColView.SelectedColumn}"; }; + listColView.KeyPress += TableViewKeyPress; + + SetupScrollBar (); + + alternatingColorScheme = new ColorScheme () { + + Disabled = Win.ColorScheme.Disabled, + HotFocus = Win.ColorScheme.HotFocus, + Focus = Win.ColorScheme.Focus, + Normal = Application.Driver.MakeAttribute (Color.White, Color.BrightBlue) + }; + + // if user clicks the mouse in TableView + listColView.MouseClick += (s, e) => { + + listColView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol); + }; + + listColView.AddKeyBinding (Key.Space, Command.ToggleChecked); + } + + private void SetupScrollBar () + { + var scrollBar = new ScrollBarView (listColView, true); // (listColView, true, true); + + scrollBar.ChangedPosition += (s, e) => { + listColView.RowOffset = scrollBar.Position; + if (listColView.RowOffset != scrollBar.Position) { + scrollBar.Position = listColView.RowOffset; + } + listColView.SetNeedsDisplay (); + }; + /* + scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => { + listColView.ColumnOffset = scrollBar.OtherScrollBarView.Position; + if (listColView.ColumnOffset != scrollBar.OtherScrollBarView.Position) { + scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset; + } + listColView.SetNeedsDisplay (); + }; + */ + + listColView.DrawContent += (s, e) => { + scrollBar.Size = listColView.Table?.Rows ?? 0; + scrollBar.Position = listColView.RowOffset; + //scrollBar.OtherScrollBarView.Size = listColView.Table?.Columns - 1 ?? 0; + //scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset; + scrollBar.Refresh (); + }; + + } + + private void TableViewKeyPress (object sender, KeyEventEventArgs e) + { + if (e.KeyEvent.Key == Key.DeleteChar) { + + // set all selected cells to null + foreach (var pt in listColView.GetAllSelectedCells ()) { + currentTable.Rows [pt.Y] [pt.X] = DBNull.Value; + } + + listColView.Update (); + e.Handled = true; + } + + } + + private void ToggleTopline () + { + _miTopline.Checked = !_miTopline.Checked; + listColView.Style.ShowHorizontalHeaderOverline = (bool)_miTopline.Checked; + listColView.Update (); + } + private void ToggleBottomline () + { + _miBottomline.Checked = !_miBottomline.Checked; + listColView.Style.ShowHorizontalBottomline = (bool)_miBottomline.Checked; + listColView.Update (); + } + private void ToggleExpandLastColumn () + { + _miExpandLastColumn.Checked = !_miExpandLastColumn.Checked; + listColView.Style.ExpandLastColumn = (bool)_miExpandLastColumn.Checked; + + listColView.Update (); + + } + + private void ToggleAlwaysUseNormalColorForVerticalCellLines () + { + _miAlwaysUseNormalColorForVerticalCellLines.Checked = !_miAlwaysUseNormalColorForVerticalCellLines.Checked; + listColView.Style.AlwaysUseNormalColorForVerticalCellLines = (bool)_miAlwaysUseNormalColorForVerticalCellLines.Checked; + + listColView.Update (); + } + private void ToggleSmoothScrolling () + { + _miSmoothScrolling.Checked = !_miSmoothScrolling.Checked; + listColView.Style.SmoothHorizontalScrolling = (bool)_miSmoothScrolling.Checked; + + listColView.Update (); + + } + private void ToggleCellLines () + { + _miCellLines.Checked = !_miCellLines.Checked; + listColView.Style.ShowVerticalCellLines = (bool)_miCellLines.Checked; + listColView.Update (); + } + private void ToggleAlternatingColors () + { + //toggle menu item + _miAlternatingColors.Checked = !_miAlternatingColors.Checked; + + if (_miAlternatingColors.Checked == true) { + listColView.Style.RowColorGetter = (a) => { return a.RowIndex % 2 == 0 ? alternatingColorScheme : null; }; + } else { + listColView.Style.RowColorGetter = null; + } + listColView.SetNeedsDisplay (); + } + + private void ToggleInvertSelectedCellFirstCharacter () + { + //toggle menu item + _miCursor.Checked = !_miCursor.Checked; + listColView.Style.InvertSelectedCellFirstCharacter = (bool)_miCursor.Checked; + listColView.SetNeedsDisplay (); + } + + private void ToggleVerticalOrientation () + { + _miOrientVertical.Checked = !_miOrientVertical.Checked; + if ((ListTableSource)listColView.Table != null) { + ((ListTableSource)listColView.Table).Style.Orientation = (bool)_miOrientVertical.Checked ? Orientation.Vertical : Orientation.Horizontal; + listColView.SetNeedsDisplay (); + } + } + + private void ToggleScrollParallel () + { + _miScrollParallel.Checked = !_miScrollParallel.Checked; + if ((ListTableSource)listColView.Table != null) { + ((ListTableSource)listColView.Table).Style.ScrollParallel = (bool)_miScrollParallel.Checked; + listColView.SetNeedsDisplay (); + } + } + + private void SetListMinWidth () + { + RunListWidthDialog ("MinCellWidth", (s, v) => s.MinCellWidth = v, (s) => s.MinCellWidth); + listColView.SetNeedsDisplay (); + } + + private void SetListMaxWidth () + { + RunListWidthDialog ("MaxCellWidth", (s, v) => s.MaxCellWidth = v, (s) => s.MaxCellWidth); + listColView.SetNeedsDisplay (); + } + + private void RunListWidthDialog (string prompt, Action setter, Func getter) + { + var accepted = false; + var ok = new Button ("Ok", is_default: true); + ok.Clicked += (s, e) => { accepted = true; Application.RequestStop (); }; + var cancel = new Button ("Cancel"); + cancel.Clicked += (s, e) => { Application.RequestStop (); }; + var d = new Dialog (ok, cancel) { Title = prompt }; + + var tf = new TextField () { + Text = getter (listColView).ToString (), + X = 0, + Y = 1, + Width = Dim.Fill () + }; + + d.Add (tf); + tf.SetFocus (); + + Application.Run (d); + + if (accepted) { + + try { + setter (listColView, int.Parse (tf.Text.ToString ())); + } catch (Exception ex) { + MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok"); + } + } + } + + private void CloseExample () + { + listColView.Table = null; + } + + private void Quit () + { + Application.RequestStop (); + } + + private void OpenSimpleList (bool big) + { + SetTable (BuildSimpleList (big ? 1023 : 31)); + } + + private void SetTable (IList list) + { + listColView.Table = new ListTableSource (list, listColView); + if ((ListTableSource)listColView.Table != null) { + currentTable = ((ListTableSource)listColView.Table).DataTable; + } + } + + /// + /// Builds a simple list in which values are the index. This helps testing that scrolling etc is working correctly and not skipping out values when paging + /// + /// + /// + public static IList BuildSimpleList (int items) + { + var list = new List (); + + for (int i = 0; i < items; i++) { + list.Add ("Item " + i); + } + + return list; + } + } +} \ No newline at end of file diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index 960de54be..e11e3dc9a 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -9,6 +9,8 @@ using System.Globalization; using Xunit.Abstractions; using System.Reflection; using Terminal.Gui.ViewTests; +using System.Collections; +using static Terminal.Gui.SpinnerStyle; namespace Terminal.Gui.ViewsTests { @@ -1930,6 +1932,42 @@ namespace Terminal.Gui.ViewsTests { "; TestHelpers.AssertDriverContentsAre (expected, output); + tableView.Bounds = new Rect (0, 0, 25, 5); + + // revert style change + style.MinAcceptableWidth = TableView.DefaultMinAcceptableWidth; + + // Now let's test the global MaxCellWidth and MinCellWidth + tableView.Style.ExpandLastColumn = false; + tableView.MaxCellWidth = 10; + tableView.MinCellWidth = 3; + + tableView.LayoutSubviews (); + tableView.Draw (); + expected = +@" +│A │B │Very Long │ │ +├───┼───┼──────────┼────┤ +│1 │2 │aaaaaaaaaa│ │ +│1 │2 │aaa │ │ +"; + TestHelpers.AssertDriverContentsAre (expected, output); + + // MaxCellWidth limits MinCellWidth + tableView.MaxCellWidth = 5; + tableView.MinCellWidth = 10; + + tableView.LayoutSubviews (); + tableView.Draw (); + expected = +@" +│A │B │Very │ │ +├─────┼─────┼─────┼─────┤ +│1 │2 │aaaaa│ │ +│1 │2 │aaa │ │ +"; + TestHelpers.AssertDriverContentsAre (expected, output); + Application.Shutdown (); } @@ -2489,6 +2527,95 @@ A B C Assert.Null (col); } + /// + /// Builds a simple list with the requested number of string items + /// + /// + /// + public static IList BuildList (int items) + { + var list = new List (); + for (int i = 0; i < items; i++) { + list.Add ("Item " + i); + } + return list.ToArray (); + } + + [Theory, AutoInitShutdown] + [InlineData (new object [] { Orientation.Horizontal, false })] + [InlineData (new object [] { Orientation.Vertical, false })] + [InlineData (new object [] { Orientation.Horizontal, true })] + [InlineData (new object [] { Orientation.Vertical, true })] + public void TestListTableSource (Orientation orient, bool parallel) + { + var list = BuildList (16); + + var tv = new TableView (); + //tv.BeginInit (); tv.EndInit (); + tv.ColorScheme = Colors.TopLevel; + tv.Bounds = new Rect (0, 0, 25, 4); + tv.Style = new () { + ShowHeaders = false, + ShowHorizontalHeaderOverline = false, + ShowHorizontalHeaderUnderline = false + }; + var listStyle = new ListTableSource.ListColumnStyle () { + Orientation = orient, + ScrollParallel = parallel + }; + + tv.Table = new ListTableSource (list, tv, listStyle); + + tv.LayoutSubviews (); + + tv.Draw (); + + string horizPerpExpected = + @" +│Item 0│Item 1 │ +│Item 2│Item 3 │ +│Item 4│Item 5 │ +│Item 6│Item 7 │"; + + string horizParaExpected = + @" +│Item 0 │Item 1 │Item 2 │ +│Item 4 │Item 5 │Item 6 │ +│Item 8 │Item 9 │Item 10│ +│Item 12│Item 13│Item 14│"; + + string vertPerpExpected = + @" +│Item 0│Item 4│Item 8 │ +│Item 1│Item 5│Item 9 │ +│Item 2│Item 6│Item 10 │ +│Item 3│Item 7│Item 11 │"; + + string vertParaExpected = + @" +│Item 0│Item 8 │ +│Item 1│Item 9 │ +│Item 2│Item 10 │ +│Item 3│Item 11 │"; + + string expected; + if (orient == Orientation.Vertical) + if (parallel) { + expected = vertParaExpected; + } else { + expected = vertPerpExpected; + } + else { + if (parallel) { + expected = horizParaExpected; + } else { + expected = horizPerpExpected; + } + } + + TestHelpers.AssertDriverContentsAre (expected, output); + } + [Fact, AutoInitShutdown] public void TestEnumerableDataSource_BasicTypes () {