Automatic tabular data formatting is discussed in the basic output section. Here we will be covering a more comprehensive mechanism that provides far more control of tabular output, called Report<T>
.
This is an example of the automatic table formatting:
var data = Enumerable.Range(0, 10)
.Select(i => new { Value = i, Squared = i * i, String = new string('I', i)});
console.FormatTable(data);
Which emits:
Value Squared String
----------- ----------- ---------
0 0
1 1 I
2 4 II
3 9 III
4 16 IIII
5 25 IIIII
6 36 IIIIII
7 49 IIIIIII
8 64 IIIIIIII
9 81 IIIIIIIII
FormatTable
can do this automatically with any IEnumerable
. It has built in support for certain types, and will use the ToString()
output for the rest.
The properties of the enumerated type are used to determine the data columns. Attributes of the column, such as left or right alignment, are determined by the datatype of the property. The heading is determined from the property name. This is all very strightforward and convenient when the defaults are adequate.
However, the Toolkit also offers a way to customise the output. Report<T>
allows you to select what columns are shown, and to control most aspects of column formatting.
Take this example:
var files = Directory.EnumerateFiles("C:\\Windows")
.Select(f => new FileInfo(f))
.Take(5);
This is the output (truncated for brevity):
Is
Directory Read
Name Length Name Directory Only Exists
------------ --------------- ---------- ------------ ----- ------
bfsvc.exe 56832 C:\Windows C:\Windows False True Full Name: C:\Windows\bfsvc.exe
Extension: .exe
Creation Time: 22/08/2013
12:21:53
Creation Time Utc: 22/08/2013
11:21:53
Last Access Time: 22/08/2013
12:21:53
Last Access Time Utc: 22/08/2013
11:21:53
Last Write Time: 22/08/2013
12:21:47
Last Write Time Utc: 22/08/2013
11:21:47
Attributes: Archive
bootstat.dat 67584 C:\Windows C:\Windows False True Full Name: C:\Windows\bootstat.
dat
Extension: .dat
Creation Time: 22/08/2013
15:46:23
Creation Time Utc: 22/08/2013
14:46:23
Last Access Time: 22/08/2013
15:46:23
Last Access Time Utc: 22/08/2013
14:46:23
Last Write Time: 01/05/2015
07:58:35
Last Write Time Utc: 01/05/2015
06:58:35
Attributes: System, Archive
The default handling does a decent job, but we don’t need all of that data. Lets use Report<T>
to refine it.
To use Report<T>
, there is an extension method for IEnumerable
called AsReport
. This is defined in the ConsoleToolkit.ConsoleIO
namespace. Lets get a report from our file data example:
var report = files.AsReport(rep => rep
.AddColumn(f => f.Name, cc => cc.Heading("File Name"))
.AddColumn(f => f.Directory, cc => cc.Heading("Location"))
.AddColumn(f => string.Format("{0:0.0}", f.Length/1024.0), cc => cc
.Heading("Length (KiB)")
.RightAlign())
);
console.FormatTable(report);
You might notice that we are creating a new variable called report
but we just pass it into FormatTable
as we did before.
The report definition is a bit funky, so let’s go through it one line at a time:
var report = files.AsReport(rep => rep
Here we are creating the report using the AsReport
extension method. This can be applied to any IEnumerable<T>
, and you configure it using a lambda function. The example breaks the line in the middle of the lambda, just for layout purposes.
The lambda’s definition is Action<ReportParameters<T>>
, which is slightly hideous (especially if you work out what T
is in the example), but you should never have to actually look at that because, in normal usage, the compiler will infer the type for you.
ReportParameters
is a class used exclusively for configuring reports. The next line defines a column:
.AddColumn(f => f.Name, cc => cc.Heading("File Name"))
Here we have a call to ReportParameter
’s AddColumn
method. The first parameter is this lambda:
f => f.Name
This is selecting the Name
property from the T
that we are reporting on.
The second parameter is this lambda:
cc => cc.Heading("File Name")
This is how the column’s settings are defined. cc
is a column definition with the type ColumnConfig
. This defines various ways to configure a column, and we will get into those shortly. However, in this case we are just using it to define the column heading - "File Name"
, to be specific.
.AddColumn(f => f.Directory, cc => cc.Heading("Location"))
The next line defines the “Location” column.
.AddColumn(f => string.Format("{0:0.0}", f.Length/1024.0), cc => cc
.Heading("Length (KiB)")
.RightAlign())
The final line adds a column to report the file size in kibibytes. The first lambda formats the number:
f => string.Format("{0:0.0}", f.Length/1024.0)
The second one configures the column:
cc => cc
.Heading("Length (KiB)")
.RightAlign()
In this case, we are setting the column heading, as before - .Heading("Length (KiB)")
- and choosing to right align the column values with .RightAlign()
. The result looks like this:
Length
File Name Location (KiB)
------------------------ ------------ ------
bfsvc.exe C:\Windows 55.5
bootstat.dat C:\Windows 66.0
comsetup.log C:\Windows 6.3
diagerr.xml C:\Windows 20.5
diagwrn.xml C:\Windows 20.5
DirectX.log C:\Windows 9.8
DtcInstall.log C:\Windows 5.1
explorer.exe C:\Windows 2442.7
HelpPane.exe C:\Windows 978.0
hh.exe C:\Windows 17.0
mib.bin C:\Windows 42.1
msxml4-KB2758694-enu.LOG C:\Windows 251.8
notepad.exe C:\Windows 216.0
PFRO.log C:\Windows 21.6
Professional.xml C:\Windows 35.4
This is the object that allows column formatting parameters to be specified. You can see in the example above that columns are configured using a fluent interface:
cc.Heading("Length (KiB)")
.RightAlign()
Here cc
is a ColumnConfig
instance.
The following configuration options are available:
.Heading(heading) |
Used to set the column heading text. |
.RightAlign() |
Requests that the column content is right aligned. |
.LeftAlign() |
Requests that the column content is left aligned. |
.DecimalPlaces(int n) |
Requests that the column value is shown with n decimal places. This only has an effect on double and decimal values. |
.Width(int n) |
Sets a fixed width for the column. This will be used if space permits. |
.MinWidth(int n) |
Specifies the minimum width for the column. The column width will be at least this big if space permits. |
.MaxWidth(int n) |
Specifies the maximum width for the column. |
.ProportionalWidth(double |
Specifies that the column should share the spare space on the line with any other ProportionalWidth columns. The other columns will be made as compact as possible. |
There are no colour configuration options available, but the colour extension methods are supported, so you can have coloured column contents by using them in the data. This will correctly apply colour to multi-line column values. (Colours will not bleed into adjacent columns.)
It is possible to nest reports. For example:
var dirs = Directory.EnumerateDirectories("C:\\Dev")
.Where(d => Directory.EnumerateFiles(d).Count() > 2)
.Take(2);
var report = dirs.AsReport(rep => rep
.AddColumn(d => d, cc => cc.Heading("Directory"))
.AddChild(d => Directory.EnumerateFiles(d).Take(5)
.Select(f => new FileInfo(f)),
nested => nested
.AddColumn(fi => fi.Name, cc => cc.Heading("File Name"))
.AddColumn(fi => (fi.Length/1024).ToString("0.00"),
cc => cc.Heading("Length (KiB)")))
);
console.FormatTable(report);
Let’s look a bit closer at that:
var dirs = Directory.EnumerateDirectories("C:\\Dev")
.Where(d => Directory.EnumerateFiles(d).Count() > 2)
.Take(2);
Here’s the source data - the names of some directories that are not empty.
var report = dirs.AsReport(rep => rep
.AddColumn(d => d, cc => cc.Heading("Directory"))
This is simply defining a column and setting the heading, as we’ve seen before.
.AddChild(d => Directory.EnumerateFiles(d).Take(5)
.Select(f => new FileInfo(f)),
nested => nested
.AddColumn(fi => fi.Name, cc => cc.Heading("File Name"))
.AddColumn(fi => (fi.Length/1024).ToString("0.00"),
cc => cc.Heading("Length (KiB)")))
And this is the child report definition. We should look at this in more detail:
.AddChild(d => Directory.EnumerateFiles(d).Take(5)
.Select(f => new FileInfo(f)),
Here we have the source data for the nested report. Embedded in the code is a lambda that extracts some files:
d => Directory.EnumerateFiles(d).Take(5)
.Select(f => new FileInfo(f))
This takes the row from the main report (which is just a directory name in this example) as a parameter. The report in the example is based on FileInfo
objects from the directory.
nested => nested
.AddColumn(fi => fi.Name, cc => cc.Heading("File Name"))
.AddColumn(fi => (fi.Length/1024).ToString("0.00"),
cc => cc.Heading("Length (KiB)")))
The rest of the statement is just a report definition.
This is the result:
Directory
------------------------
C:\Dev\ConsoleTools
Length
File Name (KiB)
----------------------------------- ------
.gitignore 2.00
ConsoleToolkit.gpState 0.00
ConsoleToolkit.sln 8.00
ConsoleToolkit.sln.DotSettings 0.00
ConsoleToolkit.sln.DotSettings.user 325.00
C:\Dev\ConsoleToolsPages
Length
File Name (KiB)
------------ ------
.gitignore 0.00
404.html 0.00
atom.xml 0.00
changelog.md 3.00
Gemfile 0.00
As you can see, each nested report is individually formatted, so their column widths do not match. This can add a substantial runtime overhead in some cases.