В предыдущей статье я сделал предварительный обзор Snap — нашего продукта для создания отчетов, разработанного, чтобы упростить создание бизнес-документации для вас и ваших пользователей.
Сегодня мы рассмотрим, как сделать готовый отчет полностью из кода. В процессе создания приложения мы более детально рассмотрим некоторые принципы работы Snap и подробнее поговорим о его внутреннем устройстве и тех механизмах, которые мы в нем реализовали.
Итак, под катом вас ждет обещанная занимательная механика.
Подготовка приложения
Для начала создадим в Visual Studio новое Windows Forms приложение и перетащим на главную форму SnapControl из вкладки DX14.1: Reporting, которая добавится в тулбокс после установки Snap.
Теперь добавим меню, используя соответствующие пункты в списке, появляющемся при щелчке мышью по смарт-тегу контрола.
Все, скелет готов, теперь будем наращивать мышцы.
Привязка данных
Как я уже упоминал, начинать работу имеет смысл с обеспечения доступа к данным. Snap поддерживает несколько сценариев назначения источника данных. Самым очевидным является использование пользовательского интерфейса. Источники данных, добавленные через UI, присваиваются конкретному документу, но их можно будет сохранить и для других отчетов. Пользователь может самостоятельно настроить подключение, используя мастер, который можно вызвать, выбрав команду Add New Data Source на вкладке File:
Или же можно выбрать пункт Add Data Source в контекстном меню, открываемом щелчком правой кнопкой мыши по пустому месту в Data Explorer:
Мастер подготовки источников данных поддерживает внушительный список возможных поставщиков:
- 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
Но не стоит забывать, что в зависимости от выбранного поставщика может понадобиться настроить различные параметры соединения — тип идентификации, имя базы данных и т.д. Поскольку для рядового пользователя все это представляется весьма эзотерическим знанием, имеет смысл настроить систему “под ключ”.
При этом необходимо учитывать, что Snap разделяет источники данных уровня конкретного отчета и более глобальные данные уровня приложения, которые доступны для любого открытого в нем документа. Данные уровня приложения задаются через свойства SnapControl — DataSource — для главного источника данных, используемого по умолчанию, и DataSources — коллекция именованных источников данных. В качестве источника данных могут использоваться стандартные поставщики .Net, списки и XML файлы.
snapControl1.DataSource = dataSet1;
snapControl1.DataSources.Add(new DataSourceInfo("NWindDataSource2", dataSet2));
Теперь при запуске приложения Data Explorer будет отображать доступные источники данных.
Чтобы задать источники данных для конкретного отчета используется аналогичная пара свойств документа.
snapControl1.Document.BeginUpdateDataSource();
this.snapControl1.Document.DataSources.Add(new DataSourceInfo("Cars", e1List));
this.snapControl1.Document.DataSources.Add(new DataSourceInfo("Company", e2List));
snapControl1.Document.EndUpdateDataSource();
Если по какой-либо причине не удалось установить соединение с источником данных, вызовется событие SnapDocument.ConnectionError. Его обработка позволяет переопределить стандартное поведение, заключающееся в вызове мастера установки соединения для повторного запроса параметров.
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
Теперь можно приступать непосредственно к созданию отчета. Как и многие другие свои способности, механизм добавления динамического содержимого Snap унаследовал от Rich Text Editor. В качестве шаблона, который заполняется реальными данными, Snap использует поля (fields). В большинстве случаев проще позволить Snap добавить поля автоматически. Если известен код поля, его можно ввести в нужное место документа самостоятельно. Достаточно нажать клавиши Ctrl+F9 и ввести код между фигурными скобками. А можно воспользоваться API и полностью создать документ программно.
Основным полем, используемым для объединения различных элементов разметки в единую модель, на основе которой будет создана заполненная данными часть документа, является SnapList. SnapList имеет иерархическую структуру и разделяется на несколько частей, каждая из которых используется для задания различных элементов списка: хэдера, шаблона для каждой строчки и футера. Эти части могут содержать вложенные поля, формируя древовидную структуру, предоставляющую возможность создавать сложные master-detail отчеты. С помощью следующего кода можно добавить SnapList в документ и настроить его, определив заголовок списка и образец для каждой записи:
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();
}
Теперь при запуске нашего приложения мы уже будем получать заполненный данными отчет:
Также можно переключиться в режим просмотра кодов полей и посмотреть, как устроен получившийся SnapList изнутри:
Неплохо, но хотелось бы настроить внешний вид получившегося документа. Приведенный ниже код позволит придать более профессиональный вид нашему отчету:
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;
}
}
}
В результате мы получим следующую картину:
SnapList также предоставляет соответствующие свойства для группировки, сортировки и фильтрации данных:
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;
}
В итоге мы получаем полноценный отчет, созданный исключительно из кода, использующего публичный Snap API. При этом визуальное дерево всех списков в документе с учетом группировки будет отображаться в специальном элементе, предназначенном для навигации по документу — Report Explorer.
Помимо простого текста, для представления данных можно использовать целый набор специальных полей:
- SnapBarCode — отображает штриховые коды различных типов;
- SnapCheckBox — вставляет чек-бокс;
- SnapHyperlink — предназначено для вставки гиперссылок;
- SnapImage — добавляет в документ изображения;
- SnapSparkline — спарклайны;
- SnapText — вставляет форматированный текст.
Модель событий
Практически любое действие, изменяющее структуру отчета, сопровождается соответствующим событием, что позволяет контролировать весь процесс и при необходимости совершать различные дополнительные действия.
Добавление нового списка:
SnapDocument.BeforeInsertSnList — в обработчике этого события можно получить доступ к тем колонкам, которые будут вставлены в документ, и при необходимости отредактировать его, например, удалить часть элементов;
SnapDocument.PrepareSnList — предоставляет доступ к структуре добавленного списка, позволяя добавить произвольный текст или изменить его, используя описанный выше API (поменять шаблоны заголовка или строк данных, применить сортировку или наложить фильтр);
SnapDocument.AfterInsertSnList — позволяет сделать окончательные правки динамически вставленного списка, указывая позиции, в которые он будет добавлен
void OnBeforeInsertSnList(object sender, BeforeInsertSnListEventArgs e) {
e.DataFields = ShowColumnChooserDialog(e.DataFields);
}
List<DataFieldInfo> ShowColumnChooserDialog(List<DataFieldInfo> dataFields) {
ColumnChooserDialog dlg = new ColumnChooserDialog();
dlg.SetFieldList(dataFields);
dlg.ShowDialog();
return dlg.Result;
}
Добавление колонок в существующий список:
Наличие хот-зон позволяет пользователю добавлять новые колонки в уже имеющийся список. При этом будут возникать следующие события:
SnapDocument.BeforeInsertSnListColumns — в обработчике этого события можно получить доступ к тем колонкам, которые будут вставлены в список, и при необходимости изменить их, например, переупорядочить или добавить новые;
SnapDocument.PrepareSnListColumns — поднимается для каждой добавленной колонки, позволяя настраивать шаблонам заголовка и основной части элемента списка;
SnapDocument.AfterInsertSnListColumns — происходит после того, как все колонки были добавлены, возвращая итоговый список в своих аргументах. Это событие дает последнюю возможность настроить список перед генерацией документа с использованием реальных данных (например, добавить группировку или сортировку);
void OnPrepareSnListColumns(object sender, PrepareSnListColumnsEventArgs e) {
e.Header.InsertHtmlText(e.Header.Range.Start, "<u>Auto-generated header for column</u>rn");
}
Добавление вложенного списка:
Если в качестве источника данных используется иерархический объект (например, DataSet с несколькими таблицами и заданными между ними отношениями), то пользователь может использовать хот-зоны для создания master-detail отчетов. В этом случае будет подниматься следующий набор событий:
SnapDocument.BeforeInsertSnListDetail — поднимается непосредсвенно после того, как выбранные поля были брошены на хот-зону. Через аргументы предоставляет доступ как к master-списку, в который будет добавлен detail, так и в тем полям, которые будут добавляться. В обработчике этого события можно изменять как набор полей, так и их порядок (можно, к примеру, полностью очистить этот набор, так что в результате никаких измений в документе не произойдет);
SnapDocument.PrepareSnListDetail — поднимается после того, как генерация вложенного списка была завершена, позволяя программно его изменить;
SnapDocument.AfterInsertSnListDetail — поднимается, когда вложенный список был добавлен;
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();
}
Если какие-то сценарии создания отчетов остались непокрытыми или у вас возникли предложения по улучшению продукта, приглашаю вас к обсуждению в комментариях. Я постараюсь ответить на все ваши вопросы.
Автор: VYudachev