TableView.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. using NStack;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Data;
  5. using System.Linq;
  6. namespace Terminal.Gui.Views {
  7. public class ColumnStyle {
  8. /// <summary>
  9. /// Defines the default alignment for all values rendered in this column. For custom alignment based on cell contents use <see cref="AlignmentGetter"/>.
  10. /// </summary>
  11. public TextAlignment Alignment {get;set;}
  12. /// <summary>
  13. /// Defines a delegate for returning custom alignment per cell based on cell values. When specified this will override <see cref="Alignment"/>
  14. /// </summary>
  15. public Func<object,TextAlignment> AlignmentGetter;
  16. /// <summary>
  17. /// Defines a delegate for returning custom representations of cell values. If not set then <see cref="object.ToString()"/> is used. Return values from your delegate may be truncated e.g. based on <see cref="MaxWidth"/>
  18. /// </summary>
  19. public Func<object,string> RepresentationGetter;
  20. /// <summary>
  21. /// Set the maximum width of the column in characters. This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/>. Defaults to <see cref="TableView.DefaultMaxCellWidth"/>
  22. /// </summary>
  23. public int MaxWidth {get;set;} = TableView.DefaultMaxCellWidth;
  24. /// <summary>
  25. /// Set the minimum width of the column in characters. This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/> or the <see cref="MaxWidth"/>
  26. /// </summary>
  27. public int MinWidth {get;set;}
  28. /// <summary>
  29. /// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
  30. /// </summary>
  31. /// <param name="cellValue"></param>
  32. /// <returns></returns>
  33. public TextAlignment GetAlignment(object cellValue)
  34. {
  35. if(AlignmentGetter != null)
  36. return AlignmentGetter(cellValue);
  37. return Alignment;
  38. }
  39. /// <summary>
  40. /// Returns the full string to render (which may be truncated if too long) that the current style says best represents the given <paramref name="value"/>
  41. /// </summary>
  42. /// <param name="value"></param>
  43. /// <returns></returns>
  44. public string GetRepresentation (object value)
  45. {
  46. if(RepresentationGetter != null)
  47. return RepresentationGetter(value);
  48. return value?.ToString();
  49. }
  50. }
  51. /// <summary>
  52. /// Defines rendering options that affect how the table is displayed
  53. /// </summary>
  54. public class TableStyle {
  55. /// <summary>
  56. /// When scrolling down always lock the column headers in place as the first row of the table
  57. /// </summary>
  58. public bool AlwaysShowHeaders {get;set;} = false;
  59. /// <summary>
  60. /// True to render a solid line above the headers
  61. /// </summary>
  62. public bool ShowHorizontalHeaderOverline {get;set;} = true;
  63. /// <summary>
  64. /// True to render a solid line under the headers
  65. /// </summary>
  66. public bool ShowHorizontalHeaderUnderline {get;set;} = true;
  67. /// <summary>
  68. /// True to render a solid line vertical line between cells
  69. /// </summary>
  70. public bool ShowVerticalCellLines {get;set;} = true;
  71. /// <summary>
  72. /// True to render a solid line vertical line between headers
  73. /// </summary>
  74. public bool ShowVerticalHeaderLines {get;set;} = true;
  75. /// <summary>
  76. /// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc)
  77. /// </summary>
  78. public Dictionary<DataColumn,ColumnStyle> ColumnStyles {get;set; } = new Dictionary<DataColumn, ColumnStyle>();
  79. /// <summary>
  80. /// Returns the entry from <see cref="ColumnStyles"/> for the given <paramref name="col"/> or null if no custom styling is defined for it
  81. /// </summary>
  82. /// <param name="col"></param>
  83. /// <returns></returns>
  84. public ColumnStyle GetColumnStyleIfAny (DataColumn col)
  85. {
  86. return ColumnStyles.TryGetValue(col,out ColumnStyle result) ? result : null;
  87. }
  88. }
  89. /// <summary>
  90. /// View for tabular data based on a <see cref="DataTable"/>
  91. /// </summary>
  92. public class TableView : View {
  93. private int columnOffset;
  94. private int rowOffset;
  95. private int selectedRow;
  96. private int selectedColumn;
  97. private DataTable table;
  98. private TableStyle style = new TableStyle();
  99. /// <summary>
  100. /// The default maximum cell width for <see cref="TableView.MaxCellWidth"/> and <see cref="ColumnStyle.MaxWidth"/>
  101. /// </summary>
  102. public const int DefaultMaxCellWidth = 100;
  103. /// <summary>
  104. /// The data table to render in the view. Setting this property automatically updates and redraws the control.
  105. /// </summary>
  106. public DataTable Table { get => table; set {table = value; Update(); } }
  107. /// <summary>
  108. /// Contains options for changing how the table is rendered
  109. /// </summary>
  110. public TableStyle Style { get => style; set {style = value; Update(); } }
  111. /// <summary>
  112. /// Zero indexed offset for the upper left <see cref="DataColumn"/> to display in <see cref="Table"/>.
  113. /// </summary>
  114. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  115. public int ColumnOffset {
  116. get => columnOffset;
  117. //try to prevent this being set to an out of bounds column
  118. set => columnOffset = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  119. }
  120. /// <summary>
  121. /// Zero indexed offset for the <see cref="DataRow"/> to display in <see cref="Table"/> on line 2 of the control (first line being headers)
  122. /// </summary>
  123. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  124. public int RowOffset {
  125. get => rowOffset;
  126. set => rowOffset = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  127. }
  128. /// <summary>
  129. /// The index of <see cref="DataTable.Columns"/> in <see cref="Table"/> that the user has currently selected
  130. /// </summary>
  131. public int SelectedColumn {
  132. get => selectedColumn;
  133. //try to prevent this being set to an out of bounds column
  134. set => selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  135. }
  136. /// <summary>
  137. /// The index of <see cref="DataTable.Rows"/> in <see cref="Table"/> that the user has currently selected
  138. /// </summary>
  139. public int SelectedRow {
  140. get => selectedRow;
  141. set => selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  142. }
  143. /// <summary>
  144. /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
  145. /// </summary>
  146. public int MaxCellWidth { get; set; } = DefaultMaxCellWidth;
  147. /// <summary>
  148. /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
  149. /// </summary>
  150. public string NullSymbol { get; set; } = "-";
  151. /// <summary>
  152. /// The symbol to add after each cell value and header value to visually seperate values (if not using vertical gridlines)
  153. /// </summary>
  154. public char SeparatorSymbol { get; set; } = ' ';
  155. /// <summary>
  156. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
  157. /// </summary>
  158. /// <param name="table">The table to display in the control</param>
  159. public TableView (DataTable table) : this ()
  160. {
  161. this.Table = table;
  162. }
  163. /// <summary>
  164. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the <see cref="Table"/> property to begin editing
  165. /// </summary>
  166. public TableView () : base ()
  167. {
  168. CanFocus = true;
  169. }
  170. ///<inheritdoc/>
  171. public override void Redraw (Rect bounds)
  172. {
  173. Move (0, 0);
  174. var frame = Frame;
  175. // What columns to render at what X offset in viewport
  176. var columnsToRender = CalculateViewport(bounds).ToArray();
  177. Driver.SetAttribute (ColorScheme.Normal);
  178. //invalidate current row (prevents scrolling around leaving old characters in the frame
  179. Driver.AddStr (new string (' ', bounds.Width));
  180. int line = 0;
  181. if(ShouldRenderHeaders()){
  182. // Render something like:
  183. /*
  184. ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  185. │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  186. └────────────────────┴──────────┴───────────┴──────────────┴─────────┘
  187. */
  188. if(Style.ShowHorizontalHeaderOverline){
  189. RenderHeaderOverline(line,bounds.Width,columnsToRender);
  190. line++;
  191. }
  192. RenderHeaderMidline(line,bounds.Width,columnsToRender);
  193. line++;
  194. if(Style.ShowHorizontalHeaderUnderline){
  195. RenderHeaderUnderline(line,bounds.Width,columnsToRender);
  196. line++;
  197. }
  198. }
  199. //render the cells
  200. for (; line < frame.Height; line++) {
  201. ClearLine(line,bounds.Width);
  202. //work out what Row to render
  203. var rowToRender = RowOffset + (line - GetHeaderHeight());
  204. //if we have run off the end of the table
  205. if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0)
  206. continue;
  207. RenderRow(line,bounds.Width,rowToRender,columnsToRender);
  208. }
  209. }
  210. /// <summary>
  211. /// Clears a line of the console by filling it with spaces
  212. /// </summary>
  213. /// <param name="row"></param>
  214. /// <param name="width"></param>
  215. private void ClearLine(int row, int width)
  216. {
  217. Move (0, row);
  218. Driver.SetAttribute (ColorScheme.Normal);
  219. Driver.AddStr (new string (' ', width));
  220. }
  221. /// <summary>
  222. /// Returns the amount of vertical space required to display the header
  223. /// </summary>
  224. /// <returns></returns>
  225. private int GetHeaderHeight()
  226. {
  227. int heightRequired = 1;
  228. if(Style.ShowHorizontalHeaderOverline)
  229. heightRequired++;
  230. if(Style.ShowHorizontalHeaderUnderline)
  231. heightRequired++;
  232. return heightRequired;
  233. }
  234. private void RenderHeaderOverline(int row,int availableWidth, ColumnToRender[] columnsToRender)
  235. {
  236. // Renders a line above table headers (when visible) like:
  237. // ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  238. for(int c = 0;c< availableWidth;c++) {
  239. var rune = Driver.HLine;
  240. if (Style.ShowVerticalHeaderLines){
  241. if(c == 0){
  242. rune = Driver.ULCorner;
  243. }
  244. // if the next column is the start of a header
  245. else if(columnsToRender.Any(r=>r.X == c+1)){
  246. rune = Driver.TopTee;
  247. }
  248. else if(c == availableWidth -1){
  249. rune = Driver.URCorner;
  250. }
  251. }
  252. AddRuneAt(Driver,c,row,rune);
  253. }
  254. }
  255. private void RenderHeaderMidline(int row,int availableWidth, ColumnToRender[] columnsToRender)
  256. {
  257. // Renders something like:
  258. // │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  259. ClearLine(row,availableWidth);
  260. //render start of line
  261. if(style.ShowVerticalHeaderLines)
  262. AddRune(0,row,Driver.VLine);
  263. for(int i =0 ; i<columnsToRender.Length;i++) {
  264. var current = columnsToRender[i];
  265. var availableWidthForCell = GetCellWidth(columnsToRender,i,availableWidth);
  266. var colStyle = Style.GetColumnStyleIfAny(current.Column);
  267. var colName = current.Column.ColumnName;
  268. RenderSeparator(current.X-1,row,true);
  269. Move (current.X, row);
  270. Driver.AddStr(TruncateOrPad(colName,colName,availableWidthForCell ,colStyle));
  271. }
  272. //render end of line
  273. if(style.ShowVerticalHeaderLines)
  274. AddRune(availableWidth-1,row,Driver.VLine);
  275. }
  276. /// <summary>
  277. /// Calculates how much space is available to render index <paramref name="i"/> of the <paramref name="columnsToRender"/> given the remaining horizontal space
  278. /// </summary>
  279. /// <param name="columnsToRender"></param>
  280. /// <param name="i"></param>
  281. /// <param name="availableWidth"></param>
  282. private int GetCellWidth (ColumnToRender [] columnsToRender, int i,int availableWidth)
  283. {
  284. var current = columnsToRender[i];
  285. var next = i+1 < columnsToRender.Length ? columnsToRender[i+1] : null;
  286. if(next == null) {
  287. // cell can fill to end of the line
  288. return availableWidth - current.X;
  289. }
  290. else {
  291. // cell can fill up to next cell start
  292. return next.X - current.X;
  293. }
  294. }
  295. private void RenderHeaderUnderline(int row,int availableWidth, ColumnToRender[] columnsToRender)
  296. {
  297. // Renders a line below the table headers (when visible) like:
  298. // ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤
  299. for(int c = 0;c< availableWidth;c++) {
  300. var rune = Driver.HLine;
  301. if (Style.ShowVerticalHeaderLines){
  302. if(c == 0){
  303. rune = Style.ShowVerticalCellLines ? Driver.LeftTee : Driver.LLCorner;
  304. }
  305. // if the next column is the start of a header
  306. else if(columnsToRender.Any(r=>r.X == c+1)){
  307. /*TODO: is ┼ symbol in Driver?*/
  308. rune = Style.ShowVerticalCellLines ? '┼' :Driver.BottomTee;
  309. }
  310. else if(c == availableWidth -1){
  311. rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner;
  312. }
  313. }
  314. AddRuneAt(Driver,c,row,rune);
  315. }
  316. }
  317. private void RenderRow(int row, int availableWidth, int rowToRender, ColumnToRender[] columnsToRender)
  318. {
  319. //render start of line
  320. if(style.ShowVerticalCellLines)
  321. AddRune(0,row,Driver.VLine);
  322. // Render cells for each visible header for the current row
  323. for(int i=0;i< columnsToRender.Length ;i++) {
  324. var current = columnsToRender[i];
  325. var availableWidthForCell = GetCellWidth(columnsToRender,i,availableWidth);
  326. var colStyle = Style.GetColumnStyleIfAny(current.Column);
  327. // move to start of cell (in line with header positions)
  328. Move (current.X, row);
  329. // Set color scheme based on whether the current cell is the selected one
  330. bool isSelectedCell = rowToRender == SelectedRow && current.Column.Ordinal == SelectedColumn;
  331. Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
  332. var val = Table.Rows [rowToRender][current.Column];
  333. // Render the (possibly truncated) cell value
  334. var representation = GetRepresentation(val,colStyle);
  335. Driver.AddStr (TruncateOrPad(val,representation,availableWidthForCell,colStyle));
  336. // Reset color scheme to normal and render the vertical line (or space) at the end of the cell
  337. Driver.SetAttribute (ColorScheme.Normal);
  338. RenderSeparator(current.X-1,row,false);
  339. }
  340. //render end of line
  341. if(style.ShowVerticalCellLines)
  342. AddRune(availableWidth-1,row,Driver.VLine);
  343. }
  344. private void RenderSeparator(int col, int row,bool isHeader)
  345. {
  346. if(col<0)
  347. return;
  348. var renderLines = isHeader ? style.ShowVerticalHeaderLines : style.ShowVerticalCellLines;
  349. Rune symbol = renderLines ? Driver.VLine : SeparatorSymbol;
  350. AddRune(col,row,symbol);
  351. }
  352. void AddRuneAt (ConsoleDriver d,int col, int row, Rune ch)
  353. {
  354. Move (col, row);
  355. d.AddRune (ch);
  356. }
  357. /// <summary>
  358. /// Truncates or pads <paramref name="representation"/> so that it occupies a exactly <paramref name="availableHorizontalSpace"/> using the alignment specified in <paramref name="style"/> (or left if no style is defined)
  359. /// </summary>
  360. /// <param name="originalCellValue">The object in this cell of the <see cref="Table"/></param>
  361. /// <param name="representation">The string representation of <paramref name="originalCellValue"/></param>
  362. /// <param name="availableHorizontalSpace"></param>
  363. /// <param name="colStyle">Optional style indicating custom alignment for the cell</param>
  364. /// <returns></returns>
  365. private ustring TruncateOrPad (object originalCellValue,string representation, int availableHorizontalSpace, ColumnStyle colStyle)
  366. {
  367. if (string.IsNullOrEmpty (representation))
  368. return representation;
  369. // if value is not wide enough
  370. if(representation.Length < availableHorizontalSpace) {
  371. // pad it out with spaces to the given alignment
  372. int toPad = availableHorizontalSpace - representation.Length;
  373. switch(colStyle?.GetAlignment(originalCellValue) ?? TextAlignment.Left) {
  374. case TextAlignment.Left :
  375. representation = representation.PadRight(toPad);
  376. break;
  377. case TextAlignment.Right :
  378. representation = representation.PadLeft(toPad);
  379. break;
  380. // TODO: With single line cells, centered and justified are the same right?
  381. case TextAlignment.Centered :
  382. case TextAlignment.Justified :
  383. //round down
  384. representation = representation.PadRight((int)Math.Floor(toPad/2.0));
  385. //round up
  386. representation = representation.PadLeft((int)Math.Ceiling(toPad/2.0));
  387. break;
  388. }
  389. return representation;
  390. }
  391. // value is too wide
  392. return representation.Substring (0, availableHorizontalSpace);
  393. }
  394. /// <inheritdoc/>
  395. public override bool ProcessKey (KeyEvent keyEvent)
  396. {
  397. switch (keyEvent.Key) {
  398. case Key.CursorLeft:
  399. SelectedColumn--;
  400. Update ();
  401. break;
  402. case Key.CursorRight:
  403. SelectedColumn++;
  404. Update ();
  405. break;
  406. case Key.CursorDown:
  407. SelectedRow++;
  408. Update ();
  409. break;
  410. case Key.CursorUp:
  411. SelectedRow--;
  412. Update ();
  413. break;
  414. case Key.PageUp:
  415. SelectedRow -= Frame.Height;
  416. Update ();
  417. break;
  418. case Key.PageDown:
  419. SelectedRow += Frame.Height;
  420. Update ();
  421. break;
  422. case Key.Home | Key.CtrlMask:
  423. SelectedRow = 0;
  424. SelectedColumn = 0;
  425. Update ();
  426. break;
  427. case Key.Home:
  428. SelectedColumn = 0;
  429. Update ();
  430. break;
  431. case Key.End | Key.CtrlMask:
  432. //jump to end of table
  433. SelectedRow = Table == null ? 0 : Table.Rows.Count - 1;
  434. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  435. Update ();
  436. break;
  437. case Key.End:
  438. //jump to end of row
  439. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  440. Update ();
  441. break;
  442. default:
  443. // Not a keystroke we care about
  444. return false;
  445. }
  446. PositionCursor ();
  447. return true;
  448. }
  449. /// <summary>
  450. /// Updates the view to reflect changes to <see cref="Table"/> and to (<see cref="ColumnOffset"/> / <see cref="RowOffset"/>) etc
  451. /// </summary>
  452. /// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
  453. public void Update()
  454. {
  455. if(Table == null) {
  456. SetNeedsDisplay ();
  457. return;
  458. }
  459. //if user opened a large table scrolled down a lot then opened a smaller table (or API deleted a bunch of columns without telling anyone)
  460. ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0);
  461. RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0);
  462. SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0);
  463. SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0);
  464. var columnsToRender = CalculateViewport (Bounds).ToArray();
  465. var headerHeight = GetHeaderHeight();
  466. //if we have scrolled too far to the left
  467. if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) {
  468. ColumnOffset = SelectedColumn;
  469. }
  470. //if we have scrolled too far to the right
  471. if (SelectedColumn > columnsToRender.Max (r=> r.Column.Ordinal)) {
  472. ColumnOffset = SelectedColumn;
  473. }
  474. //if we have scrolled too far down
  475. if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) {
  476. RowOffset = SelectedRow;
  477. }
  478. //if we have scrolled too far up
  479. if (SelectedRow < RowOffset) {
  480. RowOffset = SelectedRow;
  481. }
  482. SetNeedsDisplay ();
  483. }
  484. /// <summary>
  485. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  486. /// </summary>
  487. /// <param name="bounds"></param>
  488. /// <param name="padding"></param>
  489. /// <returns></returns>
  490. private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding = 1)
  491. {
  492. if(Table == null)
  493. yield break;
  494. int usedSpace = 0;
  495. //if horizontal space is required at the start of the line (before the first header)
  496. if(Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines)
  497. usedSpace+=1;
  498. int availableHorizontalSpace = bounds.Width;
  499. int rowsToRender = bounds.Height;
  500. // reserved for the headers row
  501. if(ShouldRenderHeaders())
  502. rowsToRender -= GetHeaderHeight();
  503. bool first = true;
  504. foreach (var col in Table.Columns.Cast<DataColumn>().Skip (ColumnOffset)) {
  505. int startingIdxForCurrentHeader = usedSpace;
  506. var colStyle = Style.GetColumnStyleIfAny(col);
  507. // is there enough space for this column (and it's data)?
  508. usedSpace += CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding;
  509. // no (don't render it) unless its the only column we are render (that must be one massively wide column!)
  510. if (!first && usedSpace > availableHorizontalSpace)
  511. yield break;
  512. // there is space
  513. yield return new ColumnToRender(col, startingIdxForCurrentHeader);
  514. first=false;
  515. }
  516. }
  517. private bool ShouldRenderHeaders()
  518. {
  519. if(Table == null || Table.Columns.Count == 0)
  520. return false;
  521. return Style.AlwaysShowHeaders || rowOffset == 0;
  522. }
  523. /// <summary>
  524. /// Returns the maximum of the <paramref name="col"/> name and the maximum length of data that will be rendered starting at <see cref="RowOffset"/> and rendering <paramref name="rowsToRender"/>
  525. /// </summary>
  526. /// <param name="col"></param>
  527. /// <param name="rowsToRender"></param>
  528. /// <param name="colStyle"></param>
  529. /// <returns></returns>
  530. private int CalculateMaxCellWidth(DataColumn col, int rowsToRender,ColumnStyle colStyle)
  531. {
  532. int spaceRequired = col.ColumnName.Length;
  533. // if table has no rows
  534. if(RowOffset < 0)
  535. return spaceRequired;
  536. for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
  537. //expand required space if cell is bigger than the last biggest cell or header
  538. spaceRequired = Math.Max (spaceRequired, GetRepresentation(Table.Rows [i][col],colStyle).Length);
  539. }
  540. // Don't require more space than the style allows
  541. if(colStyle != null){
  542. // enforce maximum cell width based on style
  543. if(spaceRequired > colStyle.MaxWidth) {
  544. spaceRequired = colStyle.MaxWidth;
  545. }
  546. // enforce minimum cell width based on style
  547. if(spaceRequired < colStyle.MinWidth) {
  548. spaceRequired = colStyle.MinWidth;
  549. }
  550. }
  551. // enforce maximum cell width based on global table style
  552. if(spaceRequired > MaxCellWidth)
  553. spaceRequired = MaxCellWidth;
  554. return spaceRequired;
  555. }
  556. /// <summary>
  557. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  558. /// </summary>
  559. /// <param name="value"></param>
  560. /// <param name="colStyle">Optional style defining how to represent cell values</param>
  561. /// <returns></returns>
  562. private string GetRepresentation(object value,ColumnStyle colStyle)
  563. {
  564. if (value == null || value == DBNull.Value) {
  565. return NullSymbol;
  566. }
  567. return colStyle != null ? colStyle.GetRepresentation(value): value.ToString();
  568. }
  569. }
  570. /// <summary>
  571. /// Describes a desire to render a column at a given horizontal position in the UI
  572. /// </summary>
  573. internal class ColumnToRender {
  574. /// <summary>
  575. /// The column to render
  576. /// </summary>
  577. public DataColumn Column {get;set;}
  578. /// <summary>
  579. /// The horizontal position to begin rendering the column at
  580. /// </summary>
  581. public int X{get;set;}
  582. public ColumnToRender (DataColumn col, int x)
  583. {
  584. Column = col;
  585. X = x;
  586. }
  587. }
  588. }