spectreconsole / spectre.console

A .NET library that makes it easier to create beautiful console applications.
https://spectreconsole.net
MIT License
9.4k stars 496 forks source link

Render various common data structures to Tables #127

Open blakepell opened 4 years ago

blakepell commented 4 years ago

I don't know if there was a way to do this already built in, but I end up working with lots of data whether it's in a IDataReader, IEnumerable<T> or IEnumerable<dynamic> (from Dapper which I use frequently, I had to do some acrobatics to reflect on the dynamic IEnumerable but it's working out well). If this didn't exist I didn't know if something like this would be useful. I'm using this for a hobby project so I haven't thought it through API wise or refactored anything but below is a brief example of how I'm using it (apologies if the inlined SQL makes anyone cringe, it's for illustration):

In this example Terminal is just an IAnsiConsole with the additional rendering functions bolted on. I wanted to write extension methods to AnsiConsole but the static class prohibited that (and I couldn't use the partial class pattern being outside of the Spectre.Console assembly.

using var conn = await Program.Db.GetOpenDbConnection();
var results = await conn.QueryAsync(@"SELECT table_name AS 'Table',
                                        ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (in MB)',
                                        table_rows as 'Estimated Rows'
                                        FROM information_schema.TABLES
                                        WHERE table_schema = 'courier'
                                        ORDER BY(data_length + index_length) DESC");

// Can specify an optional lambda to operate on each row of whatever the data source is to format it.
Terminal.Write(results, "LightSkyBlue3_1", (row) => 
{
    row[0] = $"[bold LightSkyBlue1]{row[0]}[/]";
    row[2] = $"{row[2].FormatIfNumber(0)}";
});

image

I didn't know if anyone else would use this kind of functionality or find it desirable but I thought I'd ask. Love the project also, thank you for sharing it!


Please upvote :+1: this issue if you are interested in it.

ChrisMissal commented 4 years ago

I think some sort of extension would be pretty useful, but I don't think needs to be aware of those types. My first thought was implementing an interface, and wrapping your IDataReader.

patriksvensson commented 4 years ago

@blakepell There will not be any built-in knowledge of IDataReader or similar, but I will add some convenience methods to render POCO-objects as tables.

Using AnsiConsole directly is mostly for convenience and small apps. I recommend that you pass an IAnsiConsole around for more complex applications. This way, it's easier to unit test the functionality of your application. You can get the IAnsiConsole for AnsiConsole via AnsiConsole.Console property.

Here's a quick experiment I did for adding rows from a collection of POCO objects:

/// <summary>
/// Adds multiple rows to the table.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="table">The table to add the row to.</param>
/// <param name="values">The values to create rows from.</param>
/// <param name="columnFunc">A collection of functions that gets a column value.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Table AddRows<T>(this Table table, IEnumerable<T> values, params Func<T, object>[] columnFunc)
{
    if (table is null)
    {
        throw new ArgumentNullException(nameof(table));
    }

    if (values is null)
    {
        throw new ArgumentNullException(nameof(values));
    }

    foreach (var value in values)
    {
        var columns = new List<IRenderable>();
        foreach (var converter in columnFunc)
        {
            var column = converter(value);
            if (column == null)
            {
                columns.Add(Text.Empty);
            }
            else if (column is IRenderable renderable)
            {
                columns.Add(renderable);
            }
            else
            {
                var text = column?.ToString() ?? string.Empty;
                if (string.IsNullOrWhiteSpace(text))
                {
                    columns.Add(Text.Empty);
                }
                else
                {
                    columns.Add(new Markup(text));
                }
            }
        }

        table.AddRow(columns.ToArray());
    }

    return table;
}

Usage:

var table = new Table()
    .AddRows(
        movies,
        m => $"[red]{m.Name}[/]",
        m => $"[green]{m.BoxOffice:C}[/]",
        m =>
        {
            if (m.BoxOffice > 2000000000)
            {
                return new Panel("[green]😱[/]").BorderStyle("yellow");
            }

            return "[red]Lame[/]";
        });
MoaidHathot commented 1 year ago

@patriksvensson @blakepell, I wrote a library for Dumping any .NET object in a structured, colorful way. It uses Tables from Spectre.Console. It is highly configurable and it supports max depth, circular references, type name formatting and more.

It is called Dumpify https://github.com/MoaidHathot/Dumpify/

You can dump to Console, Debug, Trace or any custom TextWriter you want (using AnsiConsole behind the scenes). Some examples:

var moaid = new Person { FirstName = "Moaid", LastName = "Hathot" };
var haneeni = new Person { FirstName = "Haneeni", LastName = "Shibli" };

moaid.Spouse = haneeni;
haneeni.Spouse = moaid;

moaid.Dump();

image image

patriksvensson commented 1 year ago

@MoaidHathot Beautiful!