Introduction
.NET Framework includes only very basic console formatting capabilities. If you need to output a few strings, it's fine. If you want to output a table, you have to calculate column widths manually, often hardcode them. If you want to color output, you have to intersperse writing strings with setting and restoring colors. If you want to wrap words properly or combine all of the above...
The code quickly becomes an unreadable mess. It's just not fun! In GUI, we have MV*, bindings and all sorts of cool stuff. Writing console applications feels like returning to the Stone Age.
CsConsoleFormat to the rescue!
Example
Imagine you have usual Order
, OrderItem
and Customer
classes. Let's create a document which prints an order
. There're two syntaxes, you can use either.
XAML (like WPF):
<Document xmlns="urn:alba:cs-console-format"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Span Background="Yellow" Text="Order #"/>
<Span Text="{Get OrderId}"/>
<Br/>
<Span Background="Yellow" Text="Customer: "/>
<Span Text="{Get Customer.Name}"/>
<Grid Color="Gray">
<Grid.Columns>
<Column Width="Auto"/>
<Column Width="*"/>
<Column Width="Auto"/>
</Grid.Columns>
<Cell Stroke="Single Wide" Color="White">Id</Cell>
<Cell Stroke="Single Wide" Color="White">Name</Cell>
<Cell Stroke="Single Wide" Color="White">Count</Cell>
<Repeater Items="{Get OrderItems}">
<Cell>
<Span Text="{Get Id}"/>
</Cell>
<Cell>
<Span Text="{Get Name}"/>
</Cell>
<Cell Align="Right">
<Span Text="{Get Count}"/>
</Cell>
</Repeater>
</Grid>
</Document>
Document doc = ConsoleRenderer.ReadDocumentFromResource(GetType(), "Views.Order.xaml", Order);
ConsoleRenderer.RenderDocument(doc);
C# (like LINQ to XML):
using static System.ConsoleColor;
var headerThickness = new LineThickness(LineWidth.Single, LineWidth.Wide);
var doc = new Document()
.AddChildren(
new Span("Order #") { Color = Yellow },
Order.Id,
"\n",
new Span("Customer: ") { Color = Yellow },
Order.Customer.Name,
new Grid { Color = Gray }
.AddColumns(
new Column { Width = GridLength.Auto },
new Column { Width = GridLength.Star(1) },
new Column { Width = GridLength.Auto }
)
.AddChildren(
new Cell { Stroke = headerThickness }
.AddChildren("Id"),
new Cell { Stroke = headerThickness }
.AddChildren("Name"),
new Cell { Stroke = headerThickness }
.AddChildren("Count"),
Order.OrderItems.Select(item => new[] {
new Cell()
.AddChildren(item.Id),
new Cell()
.AddChildren(item.Name),
new Cell { Align = HorizontalAlignment.Right }
.AddChildren(item.Count),
})
)
);
ConsoleRenderer.RenderDocument(doc);
Features
- HTML-like elements: paragraphs, spans, tables, lists, borders, separators
- Layouts: grid, stacking, docking, wrapping, absolute
- Text formatting: foreground and background colors, character wrapping, word wrapping
- Unicode formatting: hyphens, soft hyphens, no-break hyphens, spaces, no-break spaces, zero-width spaces
- Multiple syntaxes (see examples above):
- Like WPF: XAML with one-time bindings, resources, converters, attached properties, loading documents from assembly resources
- Like LINQ to XML: C# with object initializers, setting attached properties via extension methods or indexers, adding children elements by collapsing enumerables and converting objects and strings to elements
- Drawing: geometric primitives (lines, rectangles) using box-drawing characters, color transformations (dark, light), text, images
- Internationalization: cultures are respected on every level and can be customized per-element
- Export to many formats: ANSI text, unformatted text, HTML; RTF, XPF, WPF FixedDocument, WPF FlowDocument
- JetBrains ReSharper annotations: CanBeNull, NotNull, ValueProvider, Pure, etc.
- WPF document control, document converter.
Using the Code
-
Install NuGet package Alba.CsConsoleFormat using Package Manager:
PM> Install-Package Alba.CsConsoleFormat
or .NET CLI:
> dotnet add package Alba.CsConsoleFormat
-
Add using Alba.CsConsoleFormat;
to your .cs file.
-
If you’re going to use ASCII graphics on Windows, set Console.OutputEncoding = Encoding.UTF8;
.
-
If you want to use XAML:
- Add XAML file to your project. Set its build action to “Embedded Resource”.
- Load XAML using
ConsoleRenderer.ReadDocumentFromResource
.
-
If you want to use pure C#:
- Build a document in code starting with
Document
element as a root.
-
Call ConsoleRenderer.RenderDocument
on the generated document.
Real Example
The GitHub repository of the library contains a sample project named Alba.CsConsoleFormat.Sample.ProcessManager
which can list current processes, start new processes and display help. The logic of the project is straightforward, it uses CommandLineParser
library to parse the command line and then uses System.Diagnostics.Process
class to perform its primary operations.
Here's the code for generating views: displaying error and informational messages, displaying process list in a table, displaying help. Process
class contains information about a process and BaseOptionAttribute
class about a single verb or parameter from CommandLineParser
. Note that some features of C# 6 are used, including using static
.
API for constructing a document tree is similar to LINQ to XML (System.Xml.Linq
), but AddChildren
method is used instead of constructor arguments: primitive values are converted to string
s, null
s are ignored, enumerations (IEnumerable
) are collapsed and their elements are inserted instead.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using CommandLine;
using static System.ConsoleColor;
internal class View
{
private static readonly LineThickness StrokeHeader =
new LineThickness(LineWidth.None, LineWidth.Wide);
private static readonly LineThickness StrokeRight =
new LineThickness(LineWidth.None, LineWidth.None, LineWidth.Single, LineWidth.None);
public Document Error (string message, string extra = null) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Span("Error\n") { Color = Red },
new Span(message) { Color = White },
extra != null ? $"\n\n{extra}" : null
);
public Document Info (string message) =>
new Document { Background = Black, Color = Gray }
.AddChildren(message);
public Document ProcessList (IEnumerable<Process> processes) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Grid { Stroke = StrokeHeader, StrokeColor = DarkGray }
.AddColumns(
new Column { Width = GridLength.Auto },
new Column { Width = GridLength.Auto, MaxWidth = 20 },
new Column { Width = GridLength.Star(1) },
new Column { Width = GridLength.Auto }
)
.AddChildren(
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Id"),
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Name"),
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Main Window Title"),
new Cell { Stroke = StrokeHeader, Color = White }
.AddChildren("Private Memory"),
processes.Select(process => new[] {
new Cell { Stroke = StrokeRight }
.AddChildren(process.Id),
new Cell { Stroke = StrokeRight, Color = Yellow,
TextWrap = TextWrapping.NoWrap }
.AddChildren(process.ProcessName),
new Cell { Stroke = StrokeRight, Color = White,
TextWrap = TextWrapping.NoWrap }
.AddChildren(process.MainWindowTitle),
new Cell { Stroke = LineThickness.None, Align = HorizontalAlignment.Right }
.AddChildren(process.PrivateMemorySize64.ToString("n0")),
})
)
);
public Document HelpOptionsList (IEnumerable<BaseOptionAttribute> options, string instruction) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Div { Color = White }
.AddChildren(instruction),
"",
new Grid { Stroke = LineThickness.None }
.AddColumns(GridLength.Auto, GridLength.Star(1))
.AddChildren(options.Select(OptionNameAndHelp))
);
public Document HelpAllOptionsList (ILookup<BaseOptionAttribute,
BaseOptionAttribute> verbsWithOptions, string instruction) =>
new Document { Background = Black, Color = Gray }
.AddChildren(
new Span($"{instruction}\n") { Color = White },
new Grid { Stroke = LineThickness.None }
.AddColumns(GridLength.Auto, GridLength.Star(1))
.AddChildren(
verbsWithOptions.Select(verbWithOptions => new object[] {
OptionNameAndHelp(verbWithOptions.Key),
new Grid { Stroke = LineThickness.None, Margin = new Thickness(4, 0, 0, 0) }
.Set(Grid.ColumnSpanProperty, 2)
.AddColumns(GridLength.Auto, GridLength.Star(1))
.AddChildren(verbWithOptions.Select(OptionNameAndHelp)),
})
)
);
private static object[] OptionNameAndHelp (BaseOptionAttribute option) => new[] {
new Div { Margin = new Thickness(1, 0, 1, 1), Color = Yellow, MinWidth = 14 }
.AddChildren(GetOptionSyntax(option)),
new Div { Margin = new Thickness(1, 0, 1, 1) }
.AddChildren(option.HelpText),
};
private static object GetOptionSyntax (BaseOptionAttribute option)
{
if (option is VerbOptionAttribute)
return option.LongName;
else if (option.ShortName != null) {
if (option.LongName != null)
return $"--{option.LongName}, -{option.ShortName}";
else
return $"-{option.ShortName}";
}
else if (option.LongName != null)
return $"--{option.LongName}";
else
return "";
}
}
Here's what the result looks like:
API Design Issues
I'm currently torn between possible APIs for LINQ-to-XML-like syntax:
new Document(new Div("Hello"))
— params object[]
argument in constructors of all elements new Document().AddChildren(new Div().AddChildren("Hello"))
— params object[]
argument in AddChildren
extension method for all elements new Document().AddChildren(new Div("Hello"))
— a mix of both: AddChildren
for most cases, but also string text
argument in constructors of classes which often contain only text
The reason I'm reluctant in using constructors with params object[]
is that the elements can (and often do) have initializers which set their properties, so adding children elements before properties are set, while works, breaks logical order (new Div( ... several lines of elements ... ) { Color = Yellow }
). Some classes may also have meaningful non-default constructors. However, using only AddChildren
method makes code more verbose. XElement
s don't have that problem as they're fully constructed through the params object[]
arguments which can contain both their child elements and "properties" (attributes in XML terms).
I'm currently using the third option, which is a mix of the approaches, but it's a compromise which has neither concise nor consistent API. Which syntax would you prefer?
History
License
- Library — Apache License 2.0. Article — CC-BY 4.0