DEV Community

Cover image for How to Create Simple Reports with PrintDocument in C#
Chelsea Devereaux for MESCIUS inc.

Posted on • Originally published at developer.mescius.com

How to Create Simple Reports with PrintDocument in C#

Reporting is a common task in business applications, and for that, ComponentOne includes a specialized FlexReport library that allows you to make complex reports. But sometimes, using specialized tools can be too tricky or not flexible enough.

For example, if you wanted to generate a report “from scratch” by printing text to a document word by word, a reporting library would not be the best tool.

Reporting libraries are over-engineered by design to make it quick and easy. For more flexible and from-the-ground-up solutions, ComponentOne includes another component: C1PrintDocument.

About C1PrintDocumentt - An Extended C# PrintDocument Component

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.

You can download the C1PrintDocument component as part of the C1.Win.Printing NuGet package (WinForms version) or C1.Xaml.WPF.PrintDocument (WPF version). It supports .NET 6 and higher with C# or VB.NET code.

Once you are familiar with it, you can relatively easily in code create pretty complex and flexible documents that 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
    doc.Generate();

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

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 what 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;

    _printDocument.Body.Children.Add(raContainer);

    // 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
    _printDocument.DataSchema.DataSets.Add(dsCustomers);

    // 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
    raContainer.Children.Add(raItem);

    // 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
    raItem.Children.Add(rt);

    // generate document
    _printDocument.Generate();
Enter fullscreen mode Exit fullscreen mode

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 you 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
    _printDocument.DataSchema.DataSources.Add(ds);
    _printDocument.DataSchema.DataSets.Add(dsProducts);

    // 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
    tvg.DataBinding.Grouping.Expressions.Add("Fields!FirstLetter.Value");

    // 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
    _printDocument.Body.Children.Add(rt);

    // Generate document
    _printDocument.Generate();
Enter fullscreen mode Exit fullscreen mode

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:

Employees

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
    _printDocument.DataSchema.DataSources.Add(ds);
    _printDocument.DataSchema.DataSets.Add(dsEmployers);

    // 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;

    raNotes.Children.Add(rtNoteTitle);
    raNotes.Children.Add(rtNote);

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

    // create group by city
    TableVectorGroup tvg = rt.RowGroups[2, 6];
    tvg.DataBinding.DataSource = dsEmployers;
    tvg.DataBinding.Grouping.Expressions.Add("Fields!City.Value");

    // create group by country
    tvg = rt.RowGroups[0, 8];
    tvg.DataBinding.DataSource = dsEmployers;
    tvg.DataBinding.Grouping.Expressions.Add("Fields!Country.Value");

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

    // add table to the document
    _printDocument.Body.Children.Add(rt);

    // 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
    _printDocument.Generate();
Enter fullscreen mode Exit fullscreen mode

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)";
Enter fullscreen mode Exit fullscreen mode

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;

    _printDocument.Body.Children.Add(rtCaption);

    // 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);
Enter fullscreen mode Exit fullscreen mode

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
    _printDocument.DataSchema.DataSources.Add(dataSource);
    _printDocument.DataSchema.DataSets.Add(dsSales);

    // 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());

    raCaption.Children.Add(header1);
    raCaption.Children.Add(header2);

    _printDocument.Body.Children.Add(raCaption);

    // 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
    tvg.DataBinding.Grouping.Expressions.Add("Fields!ShipCountry.Value");

    // 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;
    tvg.DataBinding.Grouping.Expressions.Add("Fields!EmployeeID.Value");

    // 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
    _printDocument.Body.Children.Add(rt);

    // generate document
    _printDocument.Generate();
Enter fullscreen mode Exit fullscreen mode

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

    _printDocument.ScriptingOptions.ExternalAssemblies.Add(typeof(Form).Assembly.ManifestModule.Name);
    _printDocument.ScriptingOptions.ExternalAssemblies.Add(typeof(C1.Win.Chart.FlexChart).Assembly.ManifestModule.Name);
    _printDocument.ScriptingOptions.ExternalAssemblies.Add("System.Data.dll");
    _printDocument.ScriptingOptions.ExternalAssemblies.Add("System.Xml.dll");

    // 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));
    _printDocument.Tags.Add(newTag);

    // 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
    _printDocument.DataSchema.DataSources.Add(dataSource);
    _printDocument.DataSchema.DataSets.Add(dsCategories);

    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;
    tvg.DataBinding.Grouping.Expressions.Add("Fields!CategoryName.Value");

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

    var raLeft = new RenderArea();
    raLeft.Children.Add(rt);

    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;
    raContainer.DataBinding.Grouping.Expressions.Add("Fields!CategoryName.Value");

    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();
    raRight.Children.Add(ri);
    raContainer.Children.Add(raLeft);
    raContainer.Children.Add(raRight);
    _printDocument.Body.Children.Add(raContainer);

    // generate document
    _printDocument.Generate();
Enter fullscreen mode Exit fullscreen mode

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()
    chart.Series.Add(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)
Enter fullscreen mode Exit fullscreen mode

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 the .NET 4.5.2 version and in .NET 5, .NET 6, and beyond.

Get the complete sample code.


Top comments (0)