FluentSpreadsheets.ClosedXML 1.6.7

dotnet add package FluentSpreadsheets.ClosedXML --version 1.6.7
NuGet\Install-Package FluentSpreadsheets.ClosedXML -Version 1.6.7
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FluentSpreadsheets.ClosedXML" Version="1.6.7" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add FluentSpreadsheets.ClosedXML --version 1.6.7
#r "nuget: FluentSpreadsheets.ClosedXML, 1.6.7"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install FluentSpreadsheets.ClosedXML as a Cake Addin
#addin nuget:?package=FluentSpreadsheets.ClosedXML&version=1.6.7

// Install FluentSpreadsheets.ClosedXML as a Cake Tool
#tool nuget:?package=FluentSpreadsheets.ClosedXML&version=1.6.7

FluentSpreadsheets

FluentSpreadsheets badge

FluentSpreadsheets.ClosedXML badge

FluentSpreadsheets.GoogleSheets badge

Overview

FluentSpreadsheets library consists of two APIs:

  • Component API
    An API that provides a set of components, that can be used for building static UI with sheet cells as building blocks, as well as base logic for drawing defined component composition on the sheet.
  • Table API
    An API that provides a set of abstractions to define tables by using rows built from components and component sources.

Examples

Component API

The base unit of component API is IComponent interface, it provides a basic interface to interact with all components.

public interface IComponent
{
    Size Size { get; }

    void Accept(IComponentVisitor visitor);
}

There are many derived interfaces from IComponent used to reflect on component type in IComponentVisitor.

All component implementations are internal, so to create an instance of a component you need to use a static class ComponentFactory.

Hint

You can import static members of ComponentFactory for cleaner code.

using static FluentSpreadsheets.ComponentFactory;

Components

Use .Label to create a label component. You can pass a string to it, or use a generic overload which will call ToString on the passed object (IFormattable overload supported).

Label("Hello, World!");
Label(2.2, CultureInfo.InvariantCulture);

Containers

Use .VStack & .HStack to stack components vertically or horizontally, they will auto scale child components' width and height respectively.

VStack
(
    HStack
    (
        Label("Hello"),
        Label(",")
    ),
    Label("Stacks!")
)

The result will be something like this:
Stacks

Stacks will automatically scale their children so they all will have an equal width/height and fill a rectangle.

Styles

Use extension methods to style components.
Styles are cascading! It means that styles applied to container will be inherited by its children (and overriden, if needed). \

Cascading behaviour does not apply to styling of single component, if you will apply style A on a component, then style B, the component will have a style equal to style B applied to style A.

  VStack
  (
      HStack
      (
          Label("Hello").WithContentAlignment(HorizontalAlignment.Trailing),
          Label(",")
      ),
      Label("Styles!").WithContentAlignment(HorizontalAlignment.Center, VerticalAlignment.Top)
  ).WithTrailingBorder(BorderType.Thin, Color.Black).WithBottomBorder(BorderType.Thin, Color.Black)

Components are immutable, when you apply a style to a component, it will return a new component with the style applied, the object you called a method on will not be changed.

The result will be something like this:
Styles

Resizing

Size values are accepted as relative multipliers to default platform's sizes (column width/row height). \

Output

Code above will only produce component composition stored as objects in memory. To render it on the sheet, you need to use IComponentRenderer<T>.

Now supported:
Excel output via "ClosedXML" library. (You will need to reference a FluentSpreadsheets.ClosedXML NuGet package)
var workbook = new XLWorkbook();
var worksheet = workbook.AddWorksheet("Sample");

var helloComponent =
    VStack
    (
        HStack
        (
            Label("Hello")
                .WithContentAlignment(HorizontalAlignment.Trailing)
                .WithTrailingBorder(BorderType.Thin, Color.Black),
            Label(",")
        ),
        Label("Styles!")
            .WithContentAlignment(HorizontalAlignment.Center, VerticalAlignment.Top)
            .WithTopBorder(BorderType.Thin, Color.Black)
            .WithRowHeight(1.7)
    ).WithBottomBorder(BorderType.Thin, Color.Black).WithTrailingBorder(BorderType.Thin, Color.Black);

var renderer = new ClosedXmlComponentRenderer();
var renderCommand = new ClosedXmlRenderCommand(worksheet, helloComponent);

await renderer.RenderAsync(renderCommand);

workbook.SaveAs("sample.xlsx");
Google Sheets output via "Google Sheets API v4" library. (You will need to referencea FluentSpreadsheets.GoogleSheets NuGet package)
var credential = GoogleCredential.FromFile("credentials.json");

var initializer = new BaseClientService.Initializer
{
  HttpClientInitializer = credential
};

var service = new SheetsService(initializer);
var renderer = new GoogleSheetComponentRenderer(service);

var helloComponent =
    VStack
    (
        HStack
        (
            Label("Hello")
                .WithContentAlignment(HorizontalAlignment.Trailing)
                .WithTrailingBorder(BorderType.Thin, Color.Black),
            Label(",")
        ),
        Label("Styles!")
            .WithContentAlignment(HorizontalAlignment.Center, VerticalAlignment.Top)
            .WithTopBorder(BorderType.Thin, Color.Black)
            .WithRowHeight(1.7)
    ).WithBottomBorder(BorderType.Thin, Color.Black).WithTrailingBorder(BorderType.Thin, Color.Black);

const string spreadsheetId = "SampleSpreadsheetId";
const string title = "SampleTitle";

var renderCommandFactory = new RenderCommandFactory(service);
var renderCommand = await renderCommandFactory.CreateAsync(spreadsheetId, title, helloComponent);

await renderer.RenderAsync(renderCommand);

Table API

Table API is based on ITable<T> interface, where T is a type of model, that is used to render a table.

To define a table you need to create a class derived from RowTable<T> and implement IEnumerable<IRowComponent> RenderRows(T model) method.

To customize rendered table override Customize method in your table class.

public readonly record struct CartItem(string Name, decimal Price, int Quantity);

public readonly record struct CartTableModel(IReadOnlyCollection<CartItem> Items);

public class CartTable : RowTable<CartTableModel>, ITableCustomizer
{
    protected override IEnumerable<IRowComponent> RenderRows(CartTableModel model)
    {
        yield return Row
        (
            Label("Product Name").WithColumnWidth(1.7),
            Label("Price"),
            Label("Quantity")
        );

        foreach (var item in model.Items)
        {
            yield return Row
            (
                Label(item.Name),
                Label(item.Price, CultureInfo.InvariantCulture),
                Label(item.Quantity)
            );
        }
    }

    public override IComponent Customize(IComponent component)
    {
        return component
            .WithBottomBorder(BorderType.Thin, Color.Black)
            .WithTrailingBorder(BorderType.Thin, Color.Black);
    }
}

Use .Render method on table instance to create a component from model.

var items = new CartItem[]
{
    new CartItem("Water", 10, 10),
    new CartItem("Bread", 20, 10),
    new CartItem("Milk", 30, 10),
    new CartItem("Eggs", 40, 10),
};

var model = new CartTableModel(items);

var table = new CartTable();

var tableComponent = table.Render(model);

If you want to customize already scaled component group, you can call a CustomizedWith modifier on it.
(ex: add a common header for a header group), you can see it's usage in a student points table example

ForEach(model.HeaderData.Labs, headerData => VStack
(
    Label(headerData.Name),
    HStack
    (
        Label("Min"),
        Label("Max")
    ),
    HStack
    (
        Label(headerData.MinPoints, CultureInfo.InvariantCulture),
        Label(headerData.MaxPoints, CultureInfo.InvariantCulture)
    )
)).CustomizedWith(x => VStack(Label("Labs"), x))

Index and IndexRange labels

As composing components result in component stretching and resizing, there is no deterministic way of addressing components using hard coded indices. Library provides label API, labels are computed in rendering process, so they contain indices and index ranges that are correct relative to stretched and resized components in sheet's grid.

You can get Index or IndexRange of certain component by accessing Index or Range property accordingly.

Inline labels

You can apply WithIndexLabel or WithIndexRangeLabel modifer which will give you out a label as an out parameter.

As composing FluentSpreadsheets layouts done as continuous object creation rather then being part of delegate body, components share same markup context and labels can be used in any components after defining them as out parameter.

HStack
(
    Label("Min"),
    Label("Max")
).WithIndexLabel(out var stackLabel)

Label elevation

Even though defining inline labels is pretty handy, they are not applicable in cases, when you need to reference some components in other prior to their definitions. This is when "label elevation" process comes in.

Label elevation is implemented via label proxies, which are created using LabelProxy static class and it's Create and CreateForRange methods. They create proxies for index labels and index range labels accordingly.

WithIndexLabel and WithIndexRangeLabel modifiers have overloads which accept proxies.

var studentNameLabel = LabelProxy.CreateForRange();

Label("Student Name")
  .WithColumnWidth(1.7)
  .WithTextColor(Color.Red)
  .WithTextWrapping()
  .WithIndexRangeLabel(studentNameLabel)

To retrieve label from proxy, access it's Label property

Label(_ => $"# - {studentNameLabel.Label.Range}")

Additionally, you can assign labels to proxies manually using AssignLabel method.

Limitations

As labels are computed during rendering process, you cannot use them with components that eagerly compute it's content, because at the time of creating the component, label value is unknown.

For example Label(string) method and string interpolation will result in UnsetLabelException being thrown.

Label($"# - {studentNameLabel.Label.Range}")

Luckily, there are index aware overloads for these kind of components, which compute their value lazily, only when rendered onto a sheet.

Label(_ => $"# - {studentNameLabel.Label.Range}")

IndexLabel with stretched and resized components

When you apply index label on components that has size larger than (1, 1) after scaling, the top left index will be assigned to the index label

FluentSpreadsheets.ClosedXML

Configuration

Use service collection extension methods to configure ClosedXML driver.

services
    .AddFluentSpreadsheets()
    .AddClosedXml();

FluentSpreadsheets.GoogleSheets

Configuration

Use service collection extension methods to configure GoogleSheets driver.

services
    .AddFluentSpreadsheets()
    .AddGoogleSheets(options => options.UseBatching(batchingOptions => batchingOptions.SimultaneousRequestCount = 200);

Batching

When rendering different components on same spreadsheet you can use batching api to reduce api calls.

Use ISheetsServiceBatchScopeFactory interface and it's CreateScope method to define batching scope.

public async Task ExecuteRenders(ISheetsServiceBatchScopeFactory scopeFactory)
{
    await using var scope = scopeFactory.CreateScope();
    
    // some rendering logic here
}

While the scope is not disposed, all render requests will be batched, only on scope disposal all the necessary GoogleSheets API calls will be executed.

As GoogleSheets API only allow to batch requests by spreadsheet, using batching api will result in any efficiency increase only when you render multiple components on same spreadsheet's sheet(s).

Batching API calls for rendering on different spreadsheets will only result in deferred execution (you might gain some "performance" due to all request being executed simultaneously, reducing sequential wait time).

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.6.7 222 11/12/2023
1.6.6 130 11/1/2023
1.6.5 116 10/12/2023
1.6.4 125 9/27/2023
1.6.3 106 9/27/2023
1.5.1 91 9/21/2023
1.4.4 284 3/22/2023
1.4.3 227 3/22/2023
1.4.2 206 3/22/2023
1.4.1 219 3/22/2023
1.4.0 203 3/21/2023
1.3.1 235 2/20/2023
1.3.0 344 11/24/2022
1.2.4 393 9/21/2022
1.2.3 384 9/21/2022
1.2.2 458 9/18/2022
1.2.1 459 9/14/2022
1.2.0 416 9/12/2022
1.1.0 413 9/11/2022
1.0.1 401 9/4/2022
1.0.0 374 8/30/2022
0.0.3-alpha 171 8/6/2022
0.0.2-alpha 167 8/5/2022
0.0.1-alpha 168 7/31/2022

Added specific renderer