Click here to Skip to main content
13,707,322 members
Click here to Skip to main content
Add your own
alternative version

Stats

5K views
7 bookmarked
Posted 1 Mar 2018
Licenced CPOL

CsConsoleFormat: Introduction

, 3 Mar 2018
Rate this:
Please Sign up or sign in to vote.
CsConsoleFormat library for .NET - Formatting in Console using modern technologies

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>
// Assuming Order.xaml is stored as an Embedded Resource in the Views folder.
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

  1. Install NuGet package Alba.CsConsoleFormat using Package Manager:

    PM> Install-Package Alba.CsConsoleFormat

    or .NET CLI:

    > dotnet add package Alba.CsConsoleFormat
  2. Add using Alba.CsConsoleFormat; to your .cs file.

  3. If you’re going to use ASCII graphics on Windows, set Console.OutputEncoding = Encoding.UTF8;.

  4. If you want to use XAML:

    1. Add XAML file to your project. Set its build action to “Embedded Resource”.
    2. Load XAML using ConsoleRenderer.ReadDocumentFromResource.
  5. If you want to use pure C#:

    1. Build a document in code starting with Document element as a root.
  6. 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 strings, nulls 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. XElements 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

  • 1.0 — First version

License

  • Library — Apache License 2.0. Article — CC-BY 4.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Athari
Software Developer
Russian Federation Russian Federation


C#, JavaScript, PHP developer.




You may also be interested in...

Pro

Comments and Discussions

 
QuestionVery Nice Pin
Daniel Vaughan6-Apr-18 1:35
memberDaniel Vaughan6-Apr-18 1:35 
GeneralMy vote of 5 Pin
Ehsan Sajjad5-Mar-18 4:30
mvpEhsan Sajjad5-Mar-18 4:30 
GeneralRe: My vote of 5 Pin
Athari5-Mar-18 7:04
memberAthari5-Mar-18 7:04 
QuestionVery nice Pin
Sacha Barber2-Mar-18 19:13
mvpSacha Barber2-Mar-18 19:13 
AnswerRe: Very nice Pin
Athari3-Mar-18 3:11
memberAthari3-Mar-18 3:11 
PraiseThank you Pin
eskuvofoto2-Mar-18 7:31
membereskuvofoto2-Mar-18 7:31 
GeneralRe: Thank you Pin
Athari2-Mar-18 9:31
memberAthari2-Mar-18 9:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.180920.1 | Last Updated 3 Mar 2018
Article Copyright 2018 by Athari
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid