Reporting is a common task in business applications. We have a specialized FlexReport control that allows you to make complex reports, but using specialized tools can be too tricky or not flexible enough. For such cases, we have a more straightforward and lighter tool -> C1PrintDocument.

For many years, the C1PrintDocument component was included in the C1Report assemblies, retired now. In the 2021 v2 release, we separated C1PrintDocument into its assembly and, at the same time, ported it to .NET 5.

The new home for C1PrintDocument and related preview controls is C1.Win.Printing NuGet package (WinForms version). At this time, the WPF version is only available for .NET 4.5.2; you can find it here: C1.Xaml.WPF.PrintDocument.

About C1PrintDocument

C1PrintDocument provides an extensive and feature-rich document object model (DOM), with support for various layouts (inline, stacked), ambient and hierarchical styles, infinitely nested tables, table of contents, detailed control over pagination (orphan/widow, page size/headers/footers, etc.), etc. On top of all that, it also has a data binding layer.

Once you are familiar with it, you can relatively easily in code create pretty complex and flexible documents which then can be viewed, printed, saved, or exported in PDF (Portable Document Format), RTF (Rich Text File), MS Excel, and other formats.

If you need to print arbitrary text with pagination and different layout options, you can start using this simple code:

Hello World" with C1PrintDocument

// create C1PrintDocument instance
var doc = new C1.C1Preview.C1PrintDocument();

// insert content
doc.Body.Children.Add(new C1.C1Preview.RenderText(doc, "Hello World!"));

//force document generation

// preview what you got with RibbonPreviewDialog
var preview = new C1.Win.RibbonPreview.C1RibbonPreviewDialog();
preview.Document = doc;

That's it. You can play with page settings in the preview dialog to see that your content reflows and can be paginated without any code from your side.

If you need a more complex report-like application, you need to define the layout of elements and use data binding to generate elements according to your data. To show how you can do that, we SimpleReports sample that can be downloaded as a zip file. Now let's talk about some individual samples and technics.

Create the Simple Report

The most uncomplicated "Customer Labels" sample shows labels flowing in the left to the right direction, as many labels on a single page as can fit.

To compose C1PrintDocument, you should use elements derived from the RenderObject class and add them to the C1PrintDocument.Body.

Children collection in the same way as you add the controls to control.Controls collection in WinForms application. Elements can be nested and use parent object binding as a data source.

RenderArea is a render object that is specifically a container for child objects. By default, when a new RenderArea is created, its width is equal to the width of its parent area.

For RenderArea, specifying the width or height as "Unit.Auto" means that the size of the children determines the appropriate size.

Within their container (parent object or document body), render objects by default are placed according to the stacking rules, determined by the value of the Stacking property of the container (document for top-level objects).

This value can be one of the following StackingRulesEnum enumeration members:

  • BlockTopToBottom: Objects are placed one beneath the other within the container. When the bottom edge of the current page is reached, a new page is added. This is the default
  • BlockLeftToRight: Objects are placed one next to another, from left to right. When the right edge of the current page is reached, a new "horizontal" page is added (a horizontal page logically extends the preceding page to the right
  • InlineLeftToRight: Objects are placed inline, one next to another, from left to right. When the right edge of the current page is reached, the sequence wraps to the next line. A new page is added when the bottom of the current page is reached

The layout of the "Customer Labels" document consists of several key elements:

  1. The outer RenderArea object "raContainer" serves as a container for all labels.
  2. The RenderArea object "raItem" representing a single label.
  3. The RenderText object "rt" representing label content.

Here is how it should look like:

Customer Labels

Let me show you some code with more details:

Customer Labels Code

// create outer render area
RenderArea raContainer = new RenderArea();

// define left-to-right flow for child elements
raContainer.Stacking = StackingRulesEnum.InlineLeftToRight;


// define data schema
DataSource ds = CreateDemoDataSource();
DataSet dsCustomers = new DataSet(ds, "SELECT CompanyName, Address, City, PostalCode, Country FROM Customers ORDER BY CompanyName");

// add data set to the document

// create render area representing single label
RenderArea raItem = new RenderArea();

// set right and bottom borders as light gray dotted lines 0.1 point wide
raItem.Style.Borders.Right = new LineDef("0.1pt", Color.LightGray, System.Drawing.Drawing2D.DashStyle.Dot);
raItem.Style.Borders.Bottom = new LineDef("0.1pt", Color.LightGray, System.Drawing.Drawing2D.DashStyle.Dot);

// set size in millimeters
raItem.Width = "40mm";
raItem.Height = "20mm";

// do not split the label into different pages
raItem.SplitVertBehavior = SplitBehaviorEnum.Never;

// set the data source
raItem.DataBinding.DataSource = dsCustomers;

// add label as a child to outer container

// define text which should be printed inside label
RenderText rt = new RenderText();
rt.Text = "[Fields!CompanyName.Value]\r\n[Fields!Address.Value]\r\n[Fields!City.Value] [Fields!PostalCode.Value]\r\n[Fields!Country.Value]";

// Add the text as a child of label

// generate document

In the above code text of each label is composed of data-bound fields using the scripting language. During document generation, the C1PrintDocument will calculate these fields and print actual values. You can read more about text expressions here.

Using the Groups

C1PrintDocument allows you to use expressions to group your data. The "Alphabetical List of Products" sample groups products by the first letter of the product name. It allows to show a nice formatted list of products:

Alphabetical List of Products

This sample layouts data in a table form. The table is created using the RenderTable class. The size of tables is not limited and determined at render time by the cell with the highest row and column numbers whose contents have been set.

Row and column indices start at zero. By default, the size of the table is equal to the width of the parent element's client area. Row heights are set automatically to match the largest content height in a row.

Alphabetical List of Products Code

// Define data schema
DataSource ds = CreateDemoDataSource();

// Create DataSet containing data selected from database
// "FirstLetter" field is the first letter of product name
DataSet dsProducts = new DataSet(ds,
        "SELECT Left(p.ProductName, 1) AS FirstLetter, p.ProductName, p.QuantityPerUnit, p.UnitsInStock, c.CategoryName " +
        "FROM Categories c, Products p " +
        "WHERE c.CategoryID = p.CategoryID " +
        "ORDER BY p.ProductName");

// Add data source and data set to the document:
// this will preserve the data binding if the document is saved as c1d/c1dx

// RenderTable class is used to create a table
RenderTable rt = new RenderTable();

// TableVectorGroup collection is used to group the data by "FirstLetter" field.
// The RowGroups property accepts two integers.
// The first value is the index of the first row included in the group (0).
// The second value is the count of rows in the group (3).
TableVectorGroup tvg = rt.RowGroups[0, 3];

// Using the DataBinding property in the TableVectorGroup, which is the base class for table row and column groups, a RenderTable can be data bound.
tvg.DataBinding.DataSource = dsProducts;

// Group by “FirstLetter” query result field

// Set header row
rt.Cells[0, 0].Text = "[Fields!FirstLetter.Value]";

// Merge cells from [0, 1] to [0, 4]
rt.Cells[0, 1].SpanCols = 4;

// Set sub-header row
rt.Cells[1, 1].Text = "Product Name:";
rt.Cells[1, 2].Text = "Category Name:";
rt.Cells[1, 3].Text = "Quantity Per Unit:";
rt.Cells[1, 4].Text = "Units In Stock:";

// Set data row
// "[Fields!ProductName.Value]" means to take the value of the "ProductName" field from the query result
rt.Cells[2, 1].Text = "[Fields!ProductName.Value]";
rt.Cells[2, 2].Text = "[Fields!CategoryName.Value]";
rt.Cells[2, 3].Text = "[Fields!QuantityPerUnit.Value]";
rt.Cells[2, 4].Text = "[Fields!UnitsInStock.Value]";

// Create a nested (second) group of rows
tvg = rt.RowGroups[2, 1];
tvg.DataBinding.DataSource = dsProducts;

// Add table to the document

// Generate document

Note: ranges of groups must not overlap.

Using the Expressions

Expressions (or scripts) are used in C1PrintDocument to extract, calculate, display, group, sort, filter, parameterize and format the contents, and extend a report's functionality. You can read more about scripts here.

Visual Basic is used as the expression language by default. To use c# change ScriptingOptions.Language property.

The "Employees" sample uses scripts to insert photos into the generated document:


The sample uses the FormatDataBindingInstanceScript property to set a script executed each time a new instance of the current RenderObject is created due to data binding resolving.

Employees Code

// define data set
DataSet dsEmployers = new DataSet(ds,
     "SELECT EmployeeID, LastName, FirstName, Title, TitleOfCourtesy, BirthDate, HireDate, Address, City, Region, PostalCode, Country, HomePhone, Extension, Notes, ReportsTo, Photo " +
     "FROM Employees " +
     "ORDER BY Country, City, FirstName, LastName");

// add data source and data set to the document: this will preserve the data binding if the document is saved as c1d/c1dx

// create table
RenderTable rt = new RenderTable();

// set header row
rt.Cells[0, 0].Text = "Country";
rt.Cells[0, 1].Text = "City";
rt.Cells[0, 2].Text = "Address";
rt.Cells[0, 3].Text = "Home Phone";

// set country row
rt.Cells[1, 0].Text = "[Fields!Country.Value]";

// set city row
rt.Cells[2, 1].Text = "[Fields!City.Value]";

// set data rows
rt.Cells[3, 0].Text = "[Fields!FirstName.Value] [Fields!LastName.Value]";
rt.Cells[3, 0].SpanCols = 2;

rt.Cells[3, 2].Text = "[Fields!Address.Value]";
rt.Cells[3, 3].Text = "[Fields!HomePhone.Value]";

RenderImage ri = new RenderImage(_printDocument);

// show all exceptions and warnings for script debug
_printDocument.ThrowExceptionOnError = true;
_printDocument.AddWarningsWhenErrorInScript = true;

// using the VB script to extract an image from query resault
ri.FormatDataBindingInstanceScript = @"
     ' get ri object
     Dim ri as RenderImage = DirectCast(RenderObject, RenderImage)

     ' get DB BLOB object as byte array
     Dim picData as Byte() = DirectCast(RenderObject.Original.DataBinding.Parent.Fields!Photo.Value, Byte())
     Const bmData As Integer = 78
     Dim ms as IO.MemoryStream = New IO.MemoryStream(picData, bmData, picData.Length - bmData)

     ' create image from stream
     ri.Image = Image.FromStream(ms)

// set image parameters
ri.Width = "30mm";
ri.Height = "30mm";
ri.Style.ImageAlign.AlignHorz = ImageAlignHorzEnum.Center;
ri.Style.ImageAlign.AlignVert = ImageAlignVertEnum.Center;

rt.Cells[4, 0].RenderObject = ri;
rt.Cells[4, 0].SpanCols = 2;
rt.Cells[4, 0].SpanRows = 2;

rt.Cells[4, 2].Text = "[Fields!Title.Value]";
rt.Cells[4, 2].Style.Parents = dataStyle;

rt.Cells[4, 3].Text = "[FormatDateTime(Fields!BirthDate.Value, DateFormat.ShortDate)]    [FormatDateTime(Fields!HireDate.Value, DateFormat.ShortDate)]";
rt.Cells[4, 3].Style.Parents = dataStyle;

// add area for notes
RenderArea raNotes = new RenderArea();

var rtNoteTitle = new RenderText();
rtNoteTitle.Text = "[Fields!FirstName.Value]`s notes:";

// add note text
var rtNote = new RenderText();
rtNote.Text = "[Fields!Notes.Value]";

// do not split the area into different pages
raNotes.SplitVertBehavior = SplitBehaviorEnum.Never;


rt.Cells[5, 2].RenderObject = raNotes;

// create group by city
TableVectorGroup tvg = rt.RowGroups[2, 6];
tvg.DataBinding.DataSource = dsEmployers;

// create group by country
tvg = rt.RowGroups[0, 8];
tvg.DataBinding.DataSource = dsEmployers;

// add data rows
tvg = rt.RowGroups[3, 5];
tvg.DataBinding.DataSource = dsEmployers;
tvg.SplitBehavior = SplitBehaviorEnum.Never;

// add table to the document

// define a cell group style for the rectangular area of table cells, you can set the style rt.UserCellGroups.Add(new UserCellGroup(new Rectangle(0, 3, 4, 3)));
rt.UserCellGroups[0].Style.Borders.All = new LineDef("0.5pt", Color.Black);

// generate document

Different objects in the C1PrintDocument hierarchy have properties accepting scripts that allow changing appearance on the fly depending on data. For example, you can use style to highlight orders worth $1000 or more with blue color using the .TextColorExpr expression:

Text Color by Condition

rt.Cells[3, 4].Style.TextColorExpr = "iif(Fields!UnitPrice.Value * Fields!Quantity.Value >= 1000, Colors.Blue, Colors.Black)";

Using the Styles

The C1PrintDocument Style property is the root style of the document with which you can set the default appearance of visual components: borders, font, line spacing of a text, etc.

The C1PrintDocument PageLayout.PageSettings property helps set page options for printing to select the size of the paper, page orientation, etc.

For document pages, you can adjust settings using the Style property of the RenderObject class: content margins, page size, etc. This property cannot be assigned. Set the Parent to that other style to use another style as the base for the current object's style.

Setting Styles

// set default style for document
_printDocument.Style.FontName = "Verdana";
_printDocument.Style.FontSize = 10;

// set margin to pages in millimeters
_printDocument.PageLayout.PageSettings.LeftMargin = "12mm";
_printDocument.PageLayout.PageSettings.RightMargin = "12mm";
_printDocument.PageLayout.PageSettings.TopMargin = "12mm";
_printDocument.PageLayout.PageSettings.BottomMargin = "12mm";

// set group header style
// for large documents it is better to create a named style for reuse
C1.C1Preview.Style headerStyle = _printDocument.Style.Children.Add();
headerStyle.FontSize = 9;
headerStyle.FontBold = true;
headerStyle.GridLines.Bottom = new LineDef("1pt", Color.Black);

// set country header style
C1.C1Preview.Style countryStyle = _printDocument.Style.Children.Add();
countryStyle.FontSize = 11;
countryStyle.FontBold = true;

// set city header style
C1.C1Preview.Style cityStyle = _printDocument.Style.Children.Add();
cityStyle.FontSize = 10;
cityStyle.FontUnderline = true;

// set document caption
var rtCaption = new RenderText();
rtCaption.Text = "Employees";

// set style parameters directly is fine for small documents or styles used just once
rtCaption.Style.FontName = "Tahoma";
rtCaption.Style.FontSize = 16;
rtCaption.Style.Padding.All = "2mm";
rtCaption.Style.BackColor = Color.LightGray;


// create table
RenderTable rt = new RenderTable();

// set top, left, bottom, right padding of content within the cell to 1 millimeter
rt.CellStyle.Padding.All = "1mm";

// set header row
rt.Cells[0, 0].Text = "Country";
rt.Cells[0, 0].Style.Spacing.Top = "2mm";
rt.Cells[0, 0].Style.Parents = headerStyle;

rt.Cells[0, 1].Text = "City";
rt.Cells[0, 1].Style.Parents = headerStyle;

rt.Cells[0, 2].Text = "Address";
rt.Cells[0, 2].Style.Parents = headerStyle;

rt.Cells[0, 3].Text = "Home Phone";
rt.Cells[0, 3].Style.Parents = headerStyle;

// set country row
rt.Cells[1, 0].Text = "[Fields!Country.Value]";
rt.Cells[1, 0].Style.Parents = countryStyle;

// set city row
rt.Cells[2, 1].Text = "[Fields!City.Value]";
rt.Cells[2, 1].Style.Parents = cityStyle;

// define style for the rectangular area of table cells from cell where row = 0, column = 3, the 4 cells width and the 3 cells height
rt.UserCellGroups.Add(new UserCellGroup(new Rectangle(0, 3, 4, 3)));

// add to cell the black border of 0.5 point width
rt.UserCellGroups[0].Style.Borders.All = new LineDef("0.5pt", Color.Black);

Using the Aggregates

For the average report, just grouping data is not enough. People usually want to see some aggregate values at the bottom. Let's see how you can add aggregates in the "Employee Sales by Country" sample:

Employee Sales by Country

Employee Sales by Country Code

// set dates
var date1 = new DateTime(2015, 9, 7, 0, 0, 0);
var date2 = new DateTime(2016, 5, 5, 0, 0, 0);

var dsSales = new DataSet(dataSource, string.Format(
      "SELECT o.ShipCountry, e.EmployeeID, e.FirstName, e.LastName, o.ShippedDate, o.Freight " +
      "FROM Employees e, Orders o " +
      "WHERE e.EmployeeID = o.EmployeeID AND o.ShippedDate IS NOT NULL AND o.ShippedDate BETWEEN #{0} 00:00:00# AND #{1} 00:00:00# " +
      "ORDER BY o.ShipCountry, e.FirstName, e.LastName, o.ShippedDate", date1.ToString(@"MM\/dd\/yyyy"), date2.ToString(@"MM\/dd\/yyyy")));

// add data source and data set to the document: this will preserve the data binding if the document is saved as c1d/c1dx

// add caption
var raCaption = new RenderArea();

var header1 = new RenderText();
header1.Text = "Employee sales by country";

var header2 = new RenderText();
header2.Text = string.Format("Between {0} and {1}", date1.ToShortDateString(), date2.ToShortDateString());



// create table
var rt = new RenderTable();

// header: country
rt.Cells[0, 0].Text = "[Fields!ShipCountry.Value]";
rt.Cells[0, 2].Text = "$[Aggregates!SumByCountry.Value]";

// data
rt.Cells[1, 0].Text = "[Fields!FirstName.Value] [Fields!LastName.Value]";
rt.Cells[1, 1].Text = "[Math.Round(Aggregates!SumByEmployee.Value / Aggregates!SumByCountry.Value * 100, 1)]%";
rt.Cells[1, 2].Text = "$[Aggregates!SumByEmployee.Value]";

// create a group by ship country to be repeated for each group
TableVectorGroup tvg = rt.RowGroups[0, 2];
tvg.DataBinding.DataSource = dsSales;

// data is grouped by the "ShipCountry" field

// Create an aggregate that will calculate the sum of "Freight" fields within each grouping by the "ShipCountry" field
 _printDocument.DataSchema.Aggregates.Add(new Aggregate("SumByCountry", "Fields!Freight.Value", tvg.DataBinding, RunningEnum.Group, AggregateFuncEnum.Sum));

tvg = rt.RowGroups[1, 1];
tvg.DataBinding.DataSource = dsSales;

// add total sum by employee aggregate value: data grouped by the "EmployeeID" field, the sum of the "Freight" field values is calculated
_printDocument.DataSchema.Aggregates.Add(new Aggregate("SumByEmployee", "Fields!Freight.Value", tvg.DataBinding, RunningEnum.Group, AggregateFuncEnum.Sum));

// add data rows
tvg = rt.RowGroups[1, 1];
tvg.DataBinding.DataSource = dsSales;

// add table to the document

// generate document

In the above code, the aggregate is constructed with this method call: new Aggregate("SumByCountry", "Fields!Freight.Value", tvg.DataBinding, RunningEnum.Group, AggregateFuncEnum.Sum). Parameters are:

  1. "SumByCountry" is the aggregate name
  2. "Fields!Freight.Value" is the expression for calculating the sum
  3. "tvg.DataBinding" is the data source for the aggregate
  4. "RunningEnum.Group" means that the aggregate has a group scope
  5. "AggregateFuncEnum.Sum" means the aggregate returns the sum of values of the expression within the scope

The C1PrintDocument.DataSchema.Aggregates.Add method adds Aggregate object to Aggregates collection. Then the aggregate with "SumByCountry" name can be used in any place of document like this: rt.Cells[0, 2].Text = "$[Aggregates!SumByCountry.Value]".

Adding aggregate to document.Aggregates collection is not obligatory. You can use aggregate functions directly in any expression, as shown here.

Drawing the Charts

Very often, people want to see a visual representation of their data. The "Sales by Category" sample generates charts using FlexChartcontrol and then inserts images into the document.

Here is how it looks when previewed:

Sales by Category

To use some arbitrary assembly in the C1PrintDocument scripts, it should be added to the C1PrintDocument.ScriptingOptions.ExternalAssemblies collection. To use FlexChart, we should add references to FlexChart and its dependencies.

Sales by Category Code

// adding assemblies referenced in script


// create DataTable and assign it to document tag as data source for charts

// the "dataTable" tag will be used to pass the DataTable value to the RenderImage object

Tag newTag = new Tag("dataTable", GetDataSource(), typeof(System.Data.DataTable));

// define data schema
var dataSource = CreateDemoDataSource();

var dsCategories = new DataSet(dataSource,
      "SELECT c.CategoryName, p.ProductName, p.UnitPrice, p.UnitsInStock " +
      "FROM Products p, Categories c " +
      "WHERE p.CategoryID = c.CategoryID " +
      "ORDER BY c.CategoryName, p.ProductName");

// add data source and data set to the document: this will preserve the data binding if the document is saved as c1d/c1dx

var rt = new RenderTable();

// set header 1
rt.Cells[1, 0].Text = "[Fields!CategoryName.Value]";

// set header 2
rt.Cells[2, 0].Text = "Product:";
rt.Cells[2, 1].Text = "Sales:";

// set data row
rt.Cells[3, 0].Text = "[Fields!ProductName.Value]";
rt.Cells[3, 1].Text = "[string.Format(\"{0:C}\",Fields!UnitPrice.Value * Fields!UnitsInStock.Value)]";

// create group by category name
TableVectorGroup tvg = rt.RowGroups[0, 4];
tvg.DataBinding.DataSource = dsCategories;

// add data rows
tvg = rt.RowGroups[3, 1];
tvg.DataBinding.DataSource = dsCategories;

var raLeft = new RenderArea();

RenderArea raContainer = new RenderArea();

// arrange areas side-by-side
raContainer.Stacking = StackingRulesEnum.InlineLeftToRight;

// try to keep single category on a single page
raContainer.SplitVertBehavior = SplitBehaviorEnum.SplitIfLarge;

// set data bindings
raContainer.DataBinding.DataSource = dsCategories;

RenderImage ri = new RenderImage(_printDocument);

// show all exceptions and warnings for script debug
_printDocument.ThrowExceptionOnError = true;
_printDocument.AddWarningsWhenErrorInScript = true;

ri.FormatDataBindingInstanceScript = @"! see VB script below !";

RenderArea raRight = new RenderArea();

// generate document

The Visual Basic script (used in FormatDataBindingInstanceScript in the above code) creates a chart object, assigns a data source to it, and then converts the chart to an image:

Visual Basic Script to Create the Chart

' create chart
Dim chart as C1.Win.Chart.FlexChart = New C1.Win.Chart.FlexChart()

' set chart parameters
chart.BindingX = ""ProductName""  ' assign the name of the property that contains X values for the series
chart.Binding = ""UnitPrice""            ' assign the name of the property for the DataSource property to use in axis labels
chart.BindingContext = New System.Windows.Forms.BindingContext()
chart.AxisX.Style.Font = new System.Drawing.Font(""Tahoma"", 7, System.Drawing.FontStyle.Regular)
chart.ChartType = C1.Chart.ChartType.Column  ' show vertical bars
chart.BackColor = Color.White
chart.AxisX.OverlappingLabels = C1.Chart.OverlappingLabels.Auto  ' hide overlapping labels
chart.AxisX.LabelAngle = 90  ' the rotation angle of the axis labels

Dim series = new C1.Win.Chart.Series()

' set chart size
Dim size as Size = New Size(340, 270)

' assign data source
Dim documentTag = DirectCast(Document.Tags, TagCollection)
Dim dt = DirectCast(documentTag!dataTable.Value, System.Data.DataTable) ' get the "dataTable" tag
Dim dv as System.Data.DataView = New System.Data.DataView(dt)
Dim filter as String = ""CategoryName = "" & RenderObject.Original.Parent.DataBinding.Parent.Fields!CategoryName.Value & ""'""
dv.RowFilter = filter
chart.DataSource = dv

' create the chart image
Dim ms as IO.MemoryStream = New IO.MemoryStream()
chart.SaveImage(ms, C1.Win.Chart.ImageFormat.Png, size.Width, size.Height)

' assign the image to RenderImage object
Dim ri as RenderImage = DirectCast(RenderObject, RenderImage)

ri.Image = Image.FromStream(ms)

Similarly, you can add your custom assemblies to references and use your classes in document scripts.

The structure of the C1PrintDocument is hierarchical and relatively simple. The data is set using bindings and displaying in groups that define the appearance of data.

Each element inherited from RenderObject supports the execution of scripts that extend the functionality. It's a great tool to create simple reports or print some unbound text with different layout options.

We will continue supporting C1PrintDocument in .NET 4.5.2 version and in .NET 5, .NET 6, and beyond.

Get the complete sample code.

Find out more with the Working with C1PrintDocument documentation.

Try ComponentOne Studio

Download the latest version of ComponentOne Studio Enterprise

Download Now!