Snap is a new reporting platform. Part 2
In a previous article, I gave a preliminary overview of Snap, our reporting product designed to make it easier to create business documentation for you and your users.
Today we will look at how to make a ready-made report completely from the code. In the process of creating the application, we will take a closer look at some of the principles of Snap and talk in more detail about its internal structure and the mechanisms that we implemented in it.

So, under the cut you will find the promised entertaining mechanics.
Application Preparation
First, create a new Windows Forms application in Visual Studio and drag it onto the main SnapControl form from the DX14.1: Reporting tab , which will be added to the toolbox after Snap is installed.

Now add the menu using the corresponding items in the list that appears when you click on the smart tag of the control.


Everything, the skeleton is ready, now we will build muscle.

Data binding
As I already mentioned, it makes sense to start work by providing access to data. Snap supports several data source assignment scenarios. The most obvious is the use of the user interface. Data sources added through the UI are assigned to a specific document, but they can be saved for other reports. The user can independently configure the connection using the wizard, which can be invoked by selecting the Add New Data Source command on the File tab:

Or you can select the Add Data Source item in the context menu opened by right-clicking on an empty spot in Data Explorer:

Source Preparation Wizard The data is supported by an impressive list of possible suppliers:
But do not forget that depending on the selected provider, you may need to configure various connection parameters - identification type, database name, etc. Since for the average user all this seems to be very esoteric knowledge, it makes sense to configure the system “on a turn-key basis”.
It should be borne in mind that Snap separates data sources of a specific report level and more global data of the application level, which are available for any document opened in it. Application-level data is set through the SnapControl properties - DataSource - for the default main data source, and DataSources - a collection of named data sources. As a data source, standard .Net providers, lists, and XML files can be used.
Now when you start the application, Data Explorer will display the available data sources.

To specify data sources for a particular report, a similar pair of document properties is used.
If for some reason the connection to the data source cannot be established, the SnapDocument.ConnectionError event is raised. Its processing allows you to override the standard behavior, which consists in calling the connection wizard to re-request parameters.
API
Now you can proceed directly to the creation of the report. Like many of its other abilities, Snap inherited the mechanism for adding dynamic content from Rich Text Editor. As a template that is populated with real data, Snap uses fields. In most cases, it's easier to let Snap add fields automatically. If the field code is known, you can enter it at the right place in the document yourself. Just press Ctrl + F9 and enter the code between the curly braces. Or you can use the API and fully create the document programmatically.
The main field used to combine various markup elements into a single model, on the basis of which a document-filled part of the document will be created, is SnapList. SnapList has a hierarchical structure and is divided into several parts, each of which is used to specify various elements of the list: header, template for each line and footer. These parts can contain nested fields, forming a tree structure that provides the ability to create complex master-detail reports. Using the following code, you can add a SnapList to a document and customize it by defining a list title and sample for each record:
Now, when launching our application, we will already receive a report filled with data:

You can also switch to the field code view mode and see how the resulting SnapList works from the inside:

Not bad, but I would like to customize the appearance of the resulting document. The code below will give a more professional look to our report:
As a result, we get the following picture:

SnapList also provides the appropriate properties for grouping, sorting and filtering data:
As a result, we get a full-fledged report created exclusively from code using the public Snap API. In this case, the visual tree of all lists in the document, taking into account the grouping, will be displayed in a special element designed to navigate the document - Report Explorer.

In addition to plain text, a whole range of special fields can be used to represent data:
Event model
Almost any action that changes the structure of the report is accompanied by a corresponding event, which allows you to control the entire process and, if necessary, perform various additional actions.
Adding a new list:
SnapDocument.BeforeInsertSnList - in the handler of this event, you can access the columns that will be inserted into the document and, if necessary, edit it, for example, delete some of the elements;
SnapDocument.PrepareSnList - provides access to the structure of the added list, allowing you to add arbitrary text or change it using the API described above (change the header or data line patterns, apply sorting or filter);
SnapDocument.AfterInsertSnList - allows you to make final edits to a dynamically inserted list, indicating the positions to which it will be added

Adding columns to an existing list:
Having hot zones allows the user to add new columns to an existing list. In this case, the following events will occur:
SnapDocument.BeforeInsertSnListColumns - in the handler of this event, you can access the columns that will be inserted into the list and, if necessary, change them, for example, reorder or add new ones;
SnapDocument.PrepareSnListColumns - rises for each added column, allowing you to customize the header templates and the main part of the list item;
SnapDocument.AfterInsertSnListColumns - occurs after all columns have been added, returning the final list in its arguments. This event provides the last opportunity to configure the list before generating the document using real data (for example, add grouping or sorting);

Adding a nested list:
If a hierarchical object is used as a data source (for example, a DataSet with several tables and relationships defined between them), the user can use hot zones to create master-detail reports. In this case, the following set of events will be raised:
SnapDocument.BeforeInsertSnListDetail - rises immediately after the selected fields have been thrown into the hot zone. Through arguments, it provides access to both the master list to which detail will be added, and to the fields that will be added. In the handler of this event, you can change both the set of fields and their order (for example, you can completely clear this set, so that as a result no changes will occur in the document);
SnapDocument.PrepareSnListDetail - rises after the generation of the nested list has been completed, allowing you to programmatically change it;
SnapDocument.AfterInsertSnListDetail - rises when a nested list has been added;

If any reporting scenarios are left uncovered or you have any suggestions for improving the product, I invite you to discuss in the comments. I will try to answer all your questions.
Today we will look at how to make a ready-made report completely from the code. In the process of creating the application, we will take a closer look at some of the principles of Snap and talk in more detail about its internal structure and the mechanisms that we implemented in it.

So, under the cut you will find the promised entertaining mechanics.
Application Preparation
First, create a new Windows Forms application in Visual Studio and drag it onto the main SnapControl form from the DX14.1: Reporting tab , which will be added to the toolbox after Snap is installed.

Now add the menu using the corresponding items in the list that appears when you click on the smart tag of the control.


Everything, the skeleton is ready, now we will build muscle.

Data binding
As I already mentioned, it makes sense to start work by providing access to data. Snap supports several data source assignment scenarios. The most obvious is the use of the user interface. Data sources added through the UI are assigned to a specific document, but they can be saved for other reports. The user can independently configure the connection using the wizard, which can be invoked by selecting the Add New Data Source command on the File tab:

Or you can select the Add Data Source item in the context menu opened by right-clicking on an empty spot in Data Explorer:

Source Preparation Wizard The data is supported by an impressive list of possible suppliers:
- Microsoft SQL Server
- Microsoft Access 97
- Microsoft Access 2007
- Oracle
- Xml file
- SAP Sybase Advantage
- SAP Sybase ASE
- IBM DB2
- Firebird
- MySQL
- Pervasive PSQL
- PostgreSQL
- VistaDB
- Microsoft SQL Server CE
- Sqlite
But do not forget that depending on the selected provider, you may need to configure various connection parameters - identification type, database name, etc. Since for the average user all this seems to be very esoteric knowledge, it makes sense to configure the system “on a turn-key basis”.
It should be borne in mind that Snap separates data sources of a specific report level and more global data of the application level, which are available for any document opened in it. Application-level data is set through the SnapControl properties - DataSource - for the default main data source, and DataSources - a collection of named data sources. As a data source, standard .Net providers, lists, and XML files can be used.
snapControl1.DataSource = dataSet1;
snapControl1.DataSources.Add(new DataSourceInfo("NWindDataSource2", dataSet2));
Now when you start the application, Data Explorer will display the available data sources.

To specify data sources for a particular report, a similar pair of document properties is used.
snapControl1.Document.BeginUpdateDataSource();
this.snapControl1.Document.DataSources.Add(new DataSourceInfo("Cars", e1List));
this.snapControl1.Document.DataSources.Add(new DataSourceInfo("Company", e2List));
snapControl1.Document.EndUpdateDataSource();
If for some reason the connection to the data source cannot be established, the SnapDocument.ConnectionError event is raised. Its processing allows you to override the standard behavior, which consists in calling the connection wizard to re-request parameters.
void Document_ConnectionError(object sender, DevExpress.DataAccess.ConnectionErrorEventArgs e) {
Access2007ConnectionParameters parameters = (Access2007ConnectionParameters)e.ConnectionParameters;
string path = "C:\\Public\\Documents\\DevExpress Demos 14.1\\Components\\Data\\nwind.mdb";
parameters.FileName = path;
parameters.Password = "masterkey";
}
API
Now you can proceed directly to the creation of the report. Like many of its other abilities, Snap inherited the mechanism for adding dynamic content from Rich Text Editor. As a template that is populated with real data, Snap uses fields. In most cases, it's easier to let Snap add fields automatically. If the field code is known, you can enter it at the right place in the document yourself. Just press Ctrl + F9 and enter the code between the curly braces. Or you can use the API and fully create the document programmatically.
The main field used to combine various markup elements into a single model, on the basis of which a document-filled part of the document will be created, is SnapList. SnapList has a hierarchical structure and is divided into several parts, each of which is used to specify various elements of the list: header, template for each line and footer. These parts can contain nested fields, forming a tree structure that provides the ability to create complex master-detail reports. Using the following code, you can add a SnapList to a document and customize it by defining a list title and sample for each record:
void GenerateLayout(SnapDocument doc) {
//Добавляем SnapList в документ
SnapList list = doc.CreateSnList(doc.Range.End, "List");
list.BeginUpdate();
//Настраиваем источники данных
list.EditorRowLimit = 11;
list.DataSourceName = "NWindDataSource2";
list.DataMember = "Products";
// Добавляем хэдер
SnapDocument listHeader = list.ListHeader;
Table listHeaderTable = listHeader.InsertTable(listHeader.Range.End, 1, 3);
TableCellCollection listHeaderCells = listHeaderTable.FirstRow.Cells;
listHeader.InsertText(listHeaderCells[0].ContentRange.End, "Product Name");
listHeader.InsertText(listHeaderCells[1].ContentRange.End, "Units in Stock");
listHeader.InsertText(listHeaderCells[2].ContentRange.End, "Unit Price");
// Настраиваем шаблон строки данных
SnapDocument listRow = list.RowTemplate;
Table listRowTable = listRow.InsertTable(listRow.Range.End, 1, 3);
TableCellCollection listRowCells = listRowTable.FirstRow.Cells;
listRow.CreateSnText(listRowCells[0].ContentRange.End, "ProductName");
listRow.CreateSnText(listRowCells[1].ContentRange.End, "UnitsInStock");
listRow.CreateSnText(listRowCells[2].ContentRange.End, @"UnitPrice \$ $0.00");
list.EndUpdate();
list.Field.Update();
}
Now, when launching our application, we will already receive a report filled with data:

You can also switch to the field code view mode and see how the resulting SnapList works from the inside:

Not bad, but I would like to customize the appearance of the resulting document. The code below will give a more professional look to our report:
void FormatListHeader(SnapList list) {
SnapDocument header = list.ListHeader;
Table headerTable = header.Tables[0];
headerTable.SetPreferredWidth(50 * 100, WidthType.FiftiethsOfPercent);
foreach (TableRow row in headerTable.Rows) {
foreach (TableCell cell in row.Cells) {
// Применяем форматирование ячеек
cell.Borders.Left.LineColor = System.Drawing.Color.White;
cell.Borders.Right.LineColor = System.Drawing.Color.White;
cell.Borders.Top.LineColor = System.Drawing.Color.White;
cell.Borders.Bottom.LineColor = System.Drawing.Color.White;
cell.BackgroundColor = System.Drawing.Color.SteelBlue;
// Применяем форматирование текста
CharacterProperties formatting = header.BeginUpdateCharacters(cell.ContentRange);
formatting.Bold = true;
formatting.ForeColor = System.Drawing.Color.White;
header.EndUpdateCharacters(formatting);
}
}
}
void FormatRowTemplate(SnapList list) {
// Настраиваем внешний вид строки данных
SnapDocument rowTemplate = list.RowTemplate;
Table rowTable = rowTemplate.Tables[0];
rowTable.SetPreferredWidth(50 * 100, WidthType.FiftiethsOfPercent);
foreach (TableRow row in rowTable.Rows) {
foreach (TableCell cell in row.Cells) {
cell.Borders.Left.LineColor = System.Drawing.Color.Transparent;
cell.Borders.Right.LineColor = System.Drawing.Color.Transparent;
cell.Borders.Top.LineColor = System.Drawing.Color.Transparent;
cell.Borders.Bottom.LineColor = System.Drawing.Color.LightGray;
}
}
}
As a result, we get the following picture:

SnapList also provides the appropriate properties for grouping, sorting and filtering data:
SnapList.Groups;
SnapList.Sorting;
SnapList.Filters;
void FilterList(SnapList list) {
string filter = "[UnitPrice] >= 19";
if (!list.Filters.Contains(filter)) {
list.Filters.Add(filter);
}
}
void SortList(SnapList list) {
list.Sorting.Add(new SnapListGroupParam("UnitPrice", ColumnSortOrder.Descending));
}
void GroupList(SnapList list) {
// Добавляем группировку
SnapListGroupInfo group = list.Groups.CreateSnapListGroupInfo(
new SnapListGroupParam("CategoryID", ColumnSortOrder.Ascending));
list.Groups.Add(group);
// Добавляем хэдер для каждой группы
SnapDocument groupHeader = group.CreateHeader();
Table headerTable = groupHeader.InsertTable(groupHeader.Range.End, 1, 1);
headerTable.SetPreferredWidth(50 * 100, WidthType.FiftiethsOfPercent);
TableCellCollection groupHeaderCells = headerTable.FirstRow.Cells;
groupHeader.InsertText(groupHeaderCells[0].ContentRange.End, "Category ID: ");
groupHeader.CreateSnText(groupHeaderCells[0].ContentRange.End, "CategoryID");
CustomizeGroupCellsFormatting(groupHeaderCells);
// Добавляем футер для каждой группы
SnapDocument groupFooter = group.CreateFooter();
Table footerTable = groupFooter.InsertTable(groupFooter.Range.End, 1, 1);
footerTable.SetPreferredWidth(50 * 100, WidthType.FiftiethsOfPercent);
TableCellCollection groupFooterCells = footerTable.FirstRow.Cells;
groupFooter.InsertText(groupFooterCells[0].ContentRange.End, "Count = ");
groupFooter.CreateSnText(groupFooterCells[0].ContentRange.End,
@"CategoryID \sr Group \sf Count");
CustomizeGroupCellsFormatting(groupFooterCells);
}
void CustomizeGroupCellsFormatting(TableCellCollection cells) {
// Настраиваем форматирование ячеек
cells[0].BackgroundColor = System.Drawing.Color.LightGray;
cells[0].Borders.Bottom.LineColor = System.Drawing.Color.White;
cells[0].Borders.Left.LineColor = System.Drawing.Color.White;
cells[0].Borders.Right.LineColor = System.Drawing.Color.White;
cells[0].Borders.Top.LineColor = System.Drawing.Color.White;
}
As a result, we get a full-fledged report created exclusively from code using the public Snap API. In this case, the visual tree of all lists in the document, taking into account the grouping, will be displayed in a special element designed to navigate the document - Report Explorer.

In addition to plain text, a whole range of special fields can be used to represent data:
- SnapBarCode - displays barcodes of various types;
- SnapCheckBox - inserts a check box;
- SnapHyperlink - designed to insert hyperlinks;
- SnapImage - adds images to the document;
- SnapSparkline - sparklines;
- SnapText - inserts rich text.
Event model
Almost any action that changes the structure of the report is accompanied by a corresponding event, which allows you to control the entire process and, if necessary, perform various additional actions.
Adding a new list:
SnapDocument.BeforeInsertSnList - in the handler of this event, you can access the columns that will be inserted into the document and, if necessary, edit it, for example, delete some of the elements;
SnapDocument.PrepareSnList - provides access to the structure of the added list, allowing you to add arbitrary text or change it using the API described above (change the header or data line patterns, apply sorting or filter);
SnapDocument.AfterInsertSnList - allows you to make final edits to a dynamically inserted list, indicating the positions to which it will be added
void OnBeforeInsertSnList(object sender, BeforeInsertSnListEventArgs e) {
e.DataFields = ShowColumnChooserDialog(e.DataFields);
}
List ShowColumnChooserDialog(List dataFields) {
ColumnChooserDialog dlg = new ColumnChooserDialog();
dlg.SetFieldList(dataFields);
dlg.ShowDialog();
return dlg.Result;
}

Adding columns to an existing list:
Having hot zones allows the user to add new columns to an existing list. In this case, the following events will occur:
SnapDocument.BeforeInsertSnListColumns - in the handler of this event, you can access the columns that will be inserted into the list and, if necessary, change them, for example, reorder or add new ones;
SnapDocument.PrepareSnListColumns - rises for each added column, allowing you to customize the header templates and the main part of the list item;
SnapDocument.AfterInsertSnListColumns - occurs after all columns have been added, returning the final list in its arguments. This event provides the last opportunity to configure the list before generating the document using real data (for example, add grouping or sorting);
void OnPrepareSnListColumns(object sender, PrepareSnListColumnsEventArgs e) {
e.Header.InsertHtmlText(e.Header.Range.Start, "Auto-generated header for column\r\n");
}

Adding a nested list:
If a hierarchical object is used as a data source (for example, a DataSet with several tables and relationships defined between them), the user can use hot zones to create master-detail reports. In this case, the following set of events will be raised:
SnapDocument.BeforeInsertSnListDetail - rises immediately after the selected fields have been thrown into the hot zone. Through arguments, it provides access to both the master list to which detail will be added, and to the fields that will be added. In the handler of this event, you can change both the set of fields and their order (for example, you can completely clear this set, so that as a result no changes will occur in the document);
SnapDocument.PrepareSnListDetail - rises after the generation of the nested list has been completed, allowing you to programmatically change it;
SnapDocument.AfterInsertSnListDetail - rises when a nested list has been added;
void OnAfterInsertSnListDetail(object sender, AfterInsertSnListDetailEventArgs e) {
PaintTable();
snapControl1.Document.Selection = e.Master.Field.Range;
}
void PaintTable() {
SnapDocument document = snapControl1.Document;
TableCollection tables = document.Tables;
if (tables.Count == 0)
return;
document.BeginUpdate();
for (int k = 0; k < tables.Count; k++) {
Table table = tables[k];
TableCellProcessorDelegate reset = ResetCellStyle;
table.ForEachCell(reset);
TableCellProcessorDelegate setStyle = SetCellStyle;
table.ForEachCell(setStyle);
}
document.EndUpdate();
}

If any reporting scenarios are left uncovered or you have any suggestions for improving the product, I invite you to discuss in the comments. I will try to answer all your questions.