Creating an Interactive Catalog with C1Preview
Applies To:
C1Preview (part of Reports for WinForms)
Author:
John Juback
Published On:
12/13/2005
ComponentOne Preview for .NET (a.k.a. C1Preview) has been completely revised for the .NET Framework 2.0. Designed for developers who need to add sophisticated printing and print preview capabilities to their WinForms applications, this new version features an enhanced object model, a more robust and modular design, better performance, and many other improvements over its predecessor. It includes the following components:
C1PrintDocument
The core class of C1Preview's document creation engine, this is a non-graphical component representing a single generated document. It allows developers to control page settings, including size, orientation, margins, and columns, and to generate document content with a set of rendering methods. Its features include object-oriented document structure, advanced table support including nested tables, cascading style sheet support, content flow and page layers, XML document storage, advanced text formatting, and export to a variety of file formats including HTML, Adobe PDF, and Microsoft Excel.
C1PreviewPane
This control shows the pages of the document being previewed. It supports panning, zooming, navigation, and other preview operations. At design time, context menus are provided for creating standard toolbars and a status bar on the current form.
C1PrintPreviewControl
An integrated print preview control containing a preview pane, standard toolbars, a navigation panel with thumbnail and outline views, and a collapsible text search panel.
C1PrintPreviewDialog
A dialog form with a nested print preview control.
C1PreviewThumbnailView
A panel that can be attached to a preview pane to show navigable page thumbnails.
C1PreviewOutlineView
A panel that can be attached to a preview pane to show the hierarchical document outline.
C1PreviewTextSearchPanel
A panel that can be attached to a preview pane to perform text search in the document.
This article describes the implementation details of a WinForms application that was written entirely with C1Preview components. The C1SampleViewer application scans a ComponentOne installation folder for sample projects based on certain filename and directory structure conventions, constructs a C1PrintDocument that lists the sample projects grouped by product, and renders the document within a C1PrintPreviewControl component, as shown in the following illustration.
The center area of the form displays one or more document pages as they would be printed. A toolbar at the top of the form provides commands for page layout, navigation, scaling, and searching. The tab control on the left shows either thumbnail page views or an expandable outline view, as depicted here. The panel on the right provides a text search facility with a results list for navigating to matching pages. All of these user interface elements are part of a single component, C1PrintPreviewControl.
This application demonstrates several key features of C1Preview:
- Rendering styled text, images, and hyperlinks.
- Rendering nested tables.
- Defining headers and footers that track document content.
- Constructing document outlines and tables of contents.
- Building interactive forms and responding to user actions.
You can download the full source code for the C1SampleViewer application by visiting the following link:
[C1SampleViewer.zip](//cdn.mescius.io/assets/developer/blogs/legacy/c1/2005/12/C1SampleViewer.zip)
NOTE: In order to build this application, you must have Microsoft Visual Studio 2005 and C1Preview build 2.0.20053.40512 or later.
If you are new to C1Preview, you may want to try the following exercise before diving headfirst into the sample application. First, create a new C# Windows Application project add the following assemblies to the Toolbox in Visual Studio:
- C1.C1Preview.2.dll (the C1PrintDocument component)
- C1.Win.C1Preview.2.dll (WinForms user interface components)
Open the main form's designer and enlarge the form so that it is approximately 600 pixels wide and 400 pixels high. Add a C1PrintPreviewControl component to the form and set its Dock property to Fill. Next, add a C1PrintDocument component, which resides in the form's component tray. In the Properties window, set the Document property of c1PrintPreviewControl1 to c1PrintDocument1. This establishes a link between the visual component and the underlying document.
Double-click the main form's title bar to create a handler for the Load event, then add the following line to the directives at the top of the code file:
using C1.C1Preview;
Finally, add the following code to the body of the Load event handler:
RenderText rt = new RenderText();
rt.Text = "Hello, world!";
c1PrintDocument1.Body.Children.Add(rt);
c1PrintDocument1.Generate();
The first three lines append a text string to the document; the last line signals that the document is ready to be rendered. When you run the application, the preview component displays a one-page document containing the words "Hello, world!"
Note that the C1PrintDocument component does not provide any design-time interface for adding content; you must write code in order to populate it.
A useful technique for separating the code that generates the document from the code that displays it is to derive a class from C1PrintDocument, add your own content-generating members to that class, and set the Document property of the C1PrintPreviewControl component to an instance of the derived class. The C1SampleViewer application contains two classes that derive from C1PrintDocument:
C1SampleDocument
Implements a document that renders a set of sample projects.
C1SampleOptions
Implements a form that prompts for the location of sample projects and whether to include a table of contents and/or an index.
All C1PrintDocument content is represented by render objects, which derive from the RenderObject class. C1Preview provides a rich hierarchy of render objects for different types of content, such as text, images, shapes, tables, and general-purpose containers.
C1PrintDocument has a hierarchical structure. In order for a render object to become a part of the document, it must be added to the document's hierarchy at some point. The main part of the document, called the body, is represented by the Body property. To add a render object to the document body, you must add it to the Children collection of the Body property, as in the preceding code example.
A document's Body.Children collection contains all top-level render objects of the document. Each render object in turn also has a Children collection that may contain multiple levels of nested render objects. This is very similar to the structure of the .NET TreeView control, which has a Nodes property that returns a collection of top-level TreeNode objects, each of which has a Nodes property that returns a collection of child TreeNode objects, and so forth.
The following examples illustrate some of the most commonly used render objects.
Rendering Styled Text
RenderText rt = new RenderText();
rt.Text = "Arial 10pt bold, centered, with 1 cm space before";
rt.Style.Font = new Font("Arial", 10, FontStyle.Bold);
rt.Style.TextAlignHorz = AlignHorzEnum.Center;
rt.Style.Spacing.Top = "1cm";
c1PrintDocument1.Body.Children.Add(rt);
Rendering Images
RenderImage ri = new RenderImage();
ri.Image = System.Drawing.Image.FromFile("test.bmp");
c1PrintDocument1.Body.Children.Add(ri);
Rendering Hyperlinks
RenderText rt = new RenderText();
C1LinkTargetFile target = new C1LinkTargetFile("http://www.google.com");
rt.Text = "Click here";
rt.Hyperlink = new C1Hyperlink(target);
c1PrintDocument1.Body.Children.Add(rt);
Now let's turn our attention to the C1SampleViewer application. Each catalog item is constructed as a set of nested RenderTables, as shown in the following figure.
The structure of the outer table is as follows:
Row 0, Column 0
A nested RenderTable with 2 rows and 1 column. The top row contains the sample title in bold. The bottom row contains a brief description of the sample.
Row 0, Column 1
One or more image hyperlinks to a Visual Studio project or solution.
Row 1, Column 0
A nested RenderTable with 1 row and 2 columns. The left column contains a longer description of the sample. The right column contains a screenshot image, if available. If no image is available, the nested RenderTable contains only a single cell.
The nested table structure was chosen to force each catalog item to fit on one physical page, if possible, but to allow very long description paragraphs to span multiple physical pages, if necessary.
From this point forward, all code samples are taken from the class file C1SampleDocument.cs. Here is the code that creates the inner RenderTable for Row 0, Column 0:
private RenderTable CreateTitleTable(SampleEntry sample)
{
// Create a two-row RenderTable for the name and short description
RenderTable table = new RenderTable(2, 1);
// Render the sample name in the top cell
TableCell cellTitle = table.Cells[0, 0];
cellTitle.Style.Font = new Font(baseFont, 12, FontStyle.Bold);
cellTitle.Text = sample.Name;
// Render the short description in the bottom cell
TableCell cellDesc = table.Cells[1, 0];
cellDesc.Style.Font = new Font(baseFont, 10);
cellDesc.Style.Spacing.Top = "2pt";
cellDesc.Text = sample.Description;
table.Rows[0].PageBreakBehavior = PageBreakBehaviorEnum.PreferredBreak;
table.Rows[1].PageBreakBehavior = PageBreakBehaviorEnum.NoBreak;
return table;
}
(SampleEntry is a class that encapsulates the strings, images, and URLs associated with a sample project. See the full source code for details.)
Note the use of the PageBreakBehavior property to encourage page breaks before the top row (the bold title), and to disallow page breaks before the bottom row (the short description).
Here is the code that creates the inner RenderTable for Row 1, Column 0:
private RenderTable CreateDetailTable(SampleEntry sample)
{
// Create a two-column RenderTable for the detail text and optional image
RenderTable table = new RenderTable(1, 2);
// Render the sample's long description in the left cell
TableCell cellDetails = table.Cells[0, 0];
FormatTableCell(cellDetails, sample.Details, AlignHorzEnum.Left, AlignVertEnum.Top);
cellDetails.Style.TextColor = Color.DimGray;
cellDetails.RenderObject.Style.MinOrphanLines = 3;
// Render the screenshot image in the right cell, if present
if (sample.Image != null)
{
// Scale and right-align the image
TableCell cellImage = table.Cells[0, 1];
RenderImage ri = new RenderImage(sample.Image);
ri.Style.ImageAlign.BestFit = true;
ri.Style.ImageAlign.AlignHorz = ImageAlignHorzEnum.Right;
ri.Width = "100%";
ri.Height = new Unit(1.75, UnitTypeEnum.Inch);
// Nest the image in a RenderArea to keep it from being split
RenderArea ra = new RenderArea();
ra.Width = "100%";
ra.Children.Add(ri);
ra.Style.Padding.Left = "8pt";
cellImage.RenderObject = ra;
}
// Otherwise, the long description spans the entire row
else
{
table.Cols.Delete(1, 1);
table.Cols[0].Width = new Unit(100, DimensionEnum.Width);
}
table.Style.Spacing.Top = "8pt";
table.Rows[0].PageBreakBehavior = PageBreakBehaviorEnum.NoBreak;
table.Rows[0].CanSplit = true;
return table;
}
Note that screenshot images are right-aligned and scaled to a height of 1.75 inches. They are also nested within a RenderArea object to ensure that the entire image is drawn within a single physical page without any intervening page breaks.
As before, the PageBreakBehavior property is set to disallow page breaks. However, since the description string can be very long, the CanSplit property is set to true so that it may span multiple physical pages.
Finally, here is the code that creates the outer table:
private RenderObject CreateSample(SampleEntry sample)
{
// Create a RenderTable to display the sample as follows:
// Row 0, Col 0: Nested title table
// Row 0, Col 1: Links to solutions/projects
// Row 1: Nested detail/screenshot table
RenderTable table = new RenderTable(2, 2);
// Create a nested RenderTable for the sample name and description
TableCell cellTitle = table.Cells[0, 0];
cellTitle.RenderObject = CreateTitleTable(sample);
// Right-align the cell used for project hyperlinks
TableCell cellLinks = table.Cells[0, 1];
cellLinks.Style.TextAlignHorz = AlignHorzEnum.Right;
cellLinks.Style.Padding.Top = "3pt";
// Create a RenderParagraph to use as a hyperlink container
RenderParagraph para = new RenderParagraph(cellLinks.Style);
cellLinks.RenderObject = para;
// Render an image hyperlink for each solution/project
foreach (DictionaryEntry e in sample.Projects)
{
string language = e.Key.ToString(); // CS or VB
string target = e.Value.ToString(); // Full path
// Insert some spaces before the image
para.Content.AddText(" ");
// Create a hyperlink target for a specific version of Visual Studio
string vsprog = VSLauncher.Instance.GetProgram(target);
C1LinkTargetFile vslink;
if (vsprog != target)
vslink = new C1LinkTargetFile(vsprog, "", "\\"" target "\\"", "");
else
vslink = new C1LinkTargetFile(target);
// Insert the image hyperlink
ParagraphImage img = para.Content.AddImage(language);
img.Hyperlink = new C1Hyperlink(vslink, target);
}
// Create another nested RenderTable for the detail and screenshot
TableCell cellDetails = table.Cells[1, 0];
cellDetails.RenderObject = CreateDetailTable(sample);
cellDetails.SpanCols = 2;
// Set the width of the first column
table.Cols[0].Width = new Unit(75, DimensionEnum.Width);
// Modify the table's border and spacing and return it
table.Style.Borders.Top = new LineDef("1pt", Color.LightGray);
table.Style.Spacing.Bottom = "16pt";
table.CanSplitVert = true;
table.Rows[0].PageBreakBehavior = PageBreakBehaviorEnum.PreferredBreak;
table.Rows[1].PageBreakBehavior = PageBreakBehaviorEnum.NoBreak;
table.Rows[1].CanSplit = true;
return table;
}
Note the use of the RenderParagraph object to contain multiple image hyperlinks, separated by white space. Also, the SpanCols property for Row 1, Column 0 is set to 2 to indicate that the nested detail table should span both columns of the outer table.
Headers and footers use the same render objects as document body content. Typically, a RenderTable is used to partition a header or footer into left-aligned, centered, and/or right-aligned sections. In the C1SampleViewer application, the header contains left- and right-aligned string constants with a solid color border at the bottom:
private RenderObject CreatePageHeader()
{
// Render the page header as a two-column table with one row
RenderTable table = new RenderTable(1, 2);
TableCell left = table.Cells[0, 0];
TableCell right = table.Cells[0, 1];
FormatTableCell(left, "C1SampleViewer", AlignHorzEnum.Left, AlignVertEnum.Top);
FormatTableCell(right, "Created with Preview for .NET 2.0", AlignHorzEnum.Right, AlignVertEnum.Top);
table.Height = "0.5in";
table.Rows[0].Style.Borders.Bottom = new LineDef("1pt", Color.Crimson);
return table;
}
Unlike regular document body content, headers are assigned to the PageHeader property of the document's PageLayout object:
// Use the same header for all pages
PageLayout.PageHeader = CreatePageHeader();
But what if you want to display dynamic content in a header or footer, such as a page number? C1Preview supports built-in tags that you can use as placeholders within string expressions in your code. These tags are then evaluated at run time when headers and footers are rendered. The most commonly used tags are [PageNo], for the current page number, and [PageCount], for the total number of pages in the document. Here is the code that creates the footer table:
private RenderObject CreatePageFooter(string product)
{
// Render the page footer as a two-column table with one row
RenderTable table = new RenderTable(1, 2);
TableCell left = table.Cells[0, 0];
TableCell right = table.Cells[0, 1];
FormatTableCell(left, product, AlignHorzEnum.Left, AlignVertEnum.Bottom);
FormatTableCell(right, "Page [PageNo] of [PageCount]", AlignHorzEnum.Right, AlignVertEnum.Bottom);
table.Style.Borders.Top = new LineDef("1pt", Color.Crimson);
return table;
}
Note that this method accepts a string argument so that the footer can display the name of the product that corresponds to the current catalog item. But how does the document know that the current product has changed? The answer is that the application creates a new PageLayout object for each product. Each PageLayout object is assigned to a RenderArea object by means of the LayoutChangeBefore property. A RenderArea is a general-purpose container that is used to group render objects. In the sample application, each product is represented by a RenderArea that contains a child RenderText object for the product name, followed by child RenderTable objects for each sample project (created by the CreateSample method listed earlier).
Here is the code that creates the RenderArea container for a product:
private RenderObject CreateProduct(string product)
{
// Create a RenderArea to use as a container for this
// product's samples, and to manage page headers/footers
RenderArea ra = new RenderArea();
// Render the product name and add it to the RenderArea
RenderText text = new RenderText(product);
text.Style.Font = new Font(baseFont, 16, FontStyle.Bold);
text.Style.Spacing.Bottom = "20pt";
ra.Children.Add(text);
// Create a new page layout for this product and set its footer
PageLayout pl = new PageLayout();
pl.PageFooter = CreatePageFooter(product);
if (!firstPage)
{
firstPage = true;
PageNumberingChange numbers =
new PageNumberingChange(PageNumberingChangeModeEnum.Set, 1);
pl.PageFooter.PageNumberingChange = numbers;
}
// Force a page break before the RenderArea container
ra.LayoutChangeBefore = new LayoutChangeNewPage(pl);
return ra;
}
The if statement resets the current page number to 1 the first time CreateProduct is called. (It may not be 1 if a table of contents was generated.)
Note the use of the LayoutChangeNewPage object to force a page break before the container is rendered. If you wanted to implement a continuous section break as in Microsoft Word, you would use the LayoutChangeNoBreak object instead.
You can use the Outlines collection of C1PrintDocument to add document hierarchy information to the Outline tab in the navigation panel of the preview control. When the document is exported to PDF, this information is converted to bookmarks. You can also include a table of contents in your document by creating and populating a RenderToc object.
The following code excerpt illustrates how the sample application handles outlining and the table of contents:
private void RenderSampleList(SampleList list, string product)
{
// Render the container for this product and add it to the document
RenderObject roProduct = CreateProduct(product);
Body.Children.Add(roProduct);
// Add a top-level outline node for this product
int n = this.Outlines.Add(product, roProduct);
OutlineNode nodeProduct = this.Outlines[n];
// Render a top-level toc item for this product, if a toc exists
if (toc != null)
{
RenderTocItem ti = toc.AddItem(product, roProduct);
ti.Style.Font = new Font(baseFont, 10, FontStyle.Bold);
ti.Style.Spacing.Top = "10pt";
}
// Render each sample for this product
for (int i = 0; i < list.Count; i )
{
SampleEntry sample = (SampleEntry)list.GetByIndex(i);
RenderObject ro = CreateSample(sample);
// Add the sample's RenderObject to the product's container and outline node
roProduct.Children.Add(ro);
nodeProduct.Children.Add(sample.Name, ro);
// Add a second-level toc entry, if a toc exists
if (toc != null)
toc.AddItem(sample.Name, ro, 2);
}
}
The C1SampleViewer application also illustrates several C1Preview features not discussed in this article, including dictionaries, input forms (similar to PDF fillable forms), and responding to user events in the preview component.
C1Preview provides a powerful document generation and reporting engine for scenarios where content can only be obtained programmatically or where a standard data bound report does not meet your needs. For desktop applications, it includes visual components that provide a rich user interface for viewing, navigation, and search. For Web applications, you can export C1PrintDocument objects to popular file formats such as HTML and PDF.