Как применить пользовательскую логику к несоединенным областям
В некоторых ситуациях полное удаление несоединенных областей из документа во время Mail Merge нежелательно или приводит к тому, что документ выглядит неполным. Это может произойти, когда пользователю вместо полного удаления области следует отобразить отсутствие входных данных в виде сообщения.
Бывают также случаи, когда одного удаления неиспользуемого региона недостаточно, например, если перед регионом стоит заголовок или регион содержится в таблице. Если эта область не используется, то заголовок и таблица все равно останутся после удаления области, что будет выглядеть неуместно в документе.
В этой статье предлагается решение, позволяющее вручную определить, как обрабатываются неиспользуемые области в документе. Базовый код для этой функции предоставляется и может быть легко использован повторно в другом проекте.
Логика, применяемая к каждой области, определяется внутри класса, реализующего интерфейс IFieldMergingCallback. Точно так же можно настроить обработчик Mail Merge для управления объединением каждого поля, этот обработчик можно настроить для выполнения действий с каждым полем в неиспользуемой области или с регионом в целом. В этом обработчике вы можете задать код для изменения текста области, удаления узлов или пустых строк и ячеек и т.д.
В этом примере мы будем использовать документ, показанный ниже. Он содержит вложенные области и область, содержащуюся в таблице.
В качестве краткой демонстрации мы можем запустить образец базы данных в образце документа с включенным флагом MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS. Это свойство автоматически удалит несоединенные области из документа в течение mail merge.
Источник данных содержит две записи для области StoreDetails, но намеренно содержит какие-либо данные для дочерних областей ContactDetails для одной из записей. Кроме того, в области Suppliers также нет строк данных. Это приведет к тому, что в документе останутся неиспользуемые области. Результат после объединения документа с этим источником данных приведен ниже.
Как показано на рисунке, вы можете видеть, что область ContactDetails для второй записи и области Suppliers были автоматически удалены обработчиком Mail Merge, поскольку в них нет данных. Однако есть несколько проблем, из-за которых этот выходной документ выглядит неполным:
- В области ContactDetails по-прежнему остается абзац с текстом “Контактные данные”.
- В том же случае нет никаких указаний на отсутствие телефонных номеров, только пробел, который может привести к путанице.
- Таблица и заголовок, относящиеся к области Suppliers, также остаются после удаления области внутри таблицы.
Методика, представленная в этой статье, демонстрирует, как применить пользовательскую логику к каждой несоединенной области, чтобы избежать этих проблем.
Решение
Чтобы вручную применить логику к каждой неиспользуемой области документа, мы используем возможности, уже доступные в Aspose.Words.
Механизм Mail Merge предоставляет свойство удалять неиспользуемые области с помощью флага MailMergeCleanupOptions.RemoveUnusedRegions. Это можно отключить, чтобы такие области оставались нетронутыми в течение mail merge. Это позволяет нам оставлять не объединенные области в документе и обрабатывать их вручную самостоятельно.
Затем мы можем воспользоваться свойством MailMerge.FieldMergingCallback как средством применения нашей собственной пользовательской логики к этим несоединенным областям во время Mail Merge с помощью класса-обработчика, реализующего интерфейс IFieldMergingCallback.
Этот код в классе handler - единственный класс, который вам нужно будет изменить, чтобы управлять логикой, применяемой к несоединенным областям. Другой код в этом примере можно повторно использовать без изменений в любом проекте.
Этот примерный проект демонстрирует эту технику. Он включает в себя следующие шаги:
- Выполните Mail Merge в документе, используя ваш источник данных. Флаг MailMergeCleanupOptions.RemoveUnusedRegions отключен, пока мы хотим, чтобы области оставались, чтобы мы могли обрабатывать их вручную. Все области без данных будут удалены из документа.
- Вызовите метод ExecuteCustomLogicOnEmptyRegions. Этот метод представлен в этом примере. Он выполняет действия, которые позволяют вызывать указанный обработчик для каждой выделенной области. Этот метод можно использовать повторно и без изменений скопировать в любой проект, который его требует (вместе с любыми зависимыми методами).Этот метод выполняет следующие действия:
- Присваивает свойству MailMerge.FieldMergingCallback обработчика, указанного пользователем.
- Вызывает метод CreateDataSourceFromDocumentRegions, который принимает пользовательские имена областей, содержащие Document и ArrayList. Этот метод создаст фиктивный источник данных, содержащий таблицы для каждой выделенной области в документе.
- Выполняет Mail Merge для документа, используя фиктивный источник данных. Когда Mail Merge выполняется с этим источником данных, это позволяет вызывать указанный пользователем обработчик для каждой области разделения и применять пользовательскую логику
Код
Ниже приведена реализация метода ExecuteCustomLogicOnEmptyRegions. Этот метод принимает несколько параметров:
- Объект Document, содержащий несоединенные области, которые должны быть обработаны переданным обработчиком.
- Класс обработчика, который определяет логику, применяемую к несоединенным областям. Этот обработчик должен реализовывать IFieldMergingCallback интерфейс.
- Используя соответствующую перегрузку, метод может также принимать третий параметр – список названий регионов в виде строк. Если это указано, то вручную будут обрабатываться только названия регионов, остающиеся в документе, указанном в списке. Другие обнаруженные области не будут вызываться обработчиком и будут удалены автоматически. Если задана перегрузка только с двумя параметрами, каждая оставшаяся область в документе включается с помощью метода, который будет обрабатываться вручную.
Пример
Показывает, как выполнить пользовательскую логику в неиспользуемых областях с помощью указанного обработчика.
// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java | |
/** | |
* Applies logic defined in the passed handler class to all unused regions | |
* in the document. This allows to manually control how unused regions are | |
* handled in the document. | |
* | |
* @param doc The document containing unused regions. | |
* @param handler The handler which implements the IFieldMergingCallback | |
* interface and defines the logic to be applied to each unmerged | |
* region. | |
*/ | |
public static void executeCustomLogicOnEmptyRegions(Document doc, IFieldMergingCallback handler) throws Exception { | |
executeCustomLogicOnEmptyRegions(doc, handler, null); // Pass null to handle all regions found in the document. | |
} | |
/** | |
* Applies logic defined in the passed handler class to specific unused | |
* regions in the document as defined in regionsList. This allows to | |
* manually control how unused regions are handled in the document. | |
* | |
* @param doc The document containing unused regions. | |
* @param handler The handler which implements the IFieldMergingCallback | |
* interface and defines the logic to be applied to each unmerged | |
* region. | |
* @param regionsList A list of strings corresponding to the region names that are | |
* to be handled by the supplied handler class. Other regions | |
* encountered will not be handled and are removed automatically. | |
*/ | |
public static void executeCustomLogicOnEmptyRegions(Document doc, IFieldMergingCallback handler, ArrayList regionsList) throws Exception { | |
// Certain regions can be skipped from applying logic to by not adding the table name inside the CreateEmptyDataSource method. | |
// Enable this cleanup option so any regions which are not handled by the user's logic are removed automatically. | |
doc.getMailMerge().setCleanupOptions(MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS); | |
// Set the user's handler which is called for each unmerged region. | |
doc.getMailMerge().setFieldMergingCallback(handler); | |
// Execute mail merge using the dummy dataset. The dummy data source contains the table names of | |
// each unmerged region in the document (excluding ones that the user may have specified to be skipped). This will allow the handler | |
// to be called for each field in the unmerged regions. | |
doc.getMailMerge().executeWithRegions(createDataSourceFromDocumentRegions(doc, regionsList)); | |
} | |
/** | |
* A helper method that creates an empty Java disconnected ResultSet with | |
* the specified columns. | |
*/ | |
private static ResultSet createCachedRowSet(String[] columnNames) throws Exception { | |
RowSetMetaDataImpl metaData = new RowSetMetaDataImpl(); | |
metaData.setColumnCount(columnNames.length); | |
for (int i = 0; i < columnNames.length; i++) { | |
metaData.setColumnName(i + 1, columnNames[i]); | |
metaData.setColumnType(i + 1, java.sql.Types.VARCHAR); | |
} | |
CachedRowSet rowSet = RowSetProvider.newFactory().createCachedRowSet(); | |
; | |
rowSet.setMetaData(metaData); | |
return rowSet; | |
} | |
/** | |
* A helper method that adds a new row with the specified values to a | |
* disconnected ResultSet. | |
*/ | |
private static void addRow(ResultSet resultSet, String[] values) throws Exception { | |
resultSet.moveToInsertRow(); | |
for (int i = 0; i < values.length; i++) | |
resultSet.updateString(i + 1, values[i]); | |
resultSet.insertRow(); | |
// This "dance" is needed to add rows to the end of the result set properly. | |
// If I do something else then rows are either added at the front or the result | |
// set throws an exception about a deleted row during mail merge. | |
resultSet.moveToCurrentRow(); | |
resultSet.last(); | |
} |
Пример
Определяет метод, используемый для ручной обработки несоединенных областей.
// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java | |
/** | |
* Returns a DataSet object containing a DataTable for the unmerged regions | |
* in the specified document. If regionsList is null all regions found | |
* within the document are included. If an ArrayList instance is present the | |
* only the regions specified in the list that are found in the document are | |
* added. | |
*/ | |
private static DataSet createDataSourceFromDocumentRegions(Document doc, ArrayList regionsList) throws Exception { | |
final String TABLE_START_MARKER = "TableStart:"; | |
DataSet dataSet = new DataSet(); | |
String tableName = null; | |
for (String fieldName : doc.getMailMerge().getFieldNames()) { | |
if (fieldName.contains(TABLE_START_MARKER)) { | |
tableName = fieldName.substring(TABLE_START_MARKER.length()); | |
} else if (tableName != null) { | |
// Only add the table as a new DataTable if it doesn't already exists in the DataSet. | |
if (dataSet.getTables().get(tableName) == null) { | |
ResultSet resultSet = createCachedRowSet(new String[]{fieldName}); | |
// We only need to add the first field for the handler to be called for the fields in the region. | |
if (regionsList == null || regionsList.contains(tableName)) { | |
addRow(resultSet, new String[]{"FirstField"}); | |
} | |
dataSet.getTables().add(new DataTable(resultSet, tableName)); | |
} | |
tableName = null; | |
} | |
} | |
return dataSet; | |
} |
Этот метод предполагает поиск всех несоединенных областей в документе. Для этого используется метод MailMerge.GetFieldNames. Этот метод возвращает все объединяемые поля в документе, включая маркеры начала и конца области (представленные объединяемыми полями с префиксом TableStart или TableEnd).
При обнаружении поля слияния TableStart
оно добавляется как новое DataTable к DataSet. Поскольку область может отображаться более одного раза (например, потому что это вложенная область, в которой родительская область была объединена с несколькими записями), таблица создается и добавляется только в том случае, если она еще не существует в DataSet.
Когда подходящее начало региона найдено и добавлено в базу данных, к DataTable добавляется следующее поле (соответствующее первому полю в регионе). Для каждого поля в регионе, которое должно быть объединено и передано обработчику, требуется добавить только первое поле.
Мы также установили значение первого поля равным “FirstField”, чтобы упростить применение логики к первому или другим полям в регионе. Включение этого параметра означает, что нет необходимости жестко кодировать имя первого поля или реализовывать дополнительный код для проверки того, является ли текущее поле первым в коде обработчика.
Приведенный ниже код демонстрирует, как работает эта система. Документ, показанный в начале этой статьи, повторно объединен с тем же источником данных, но на этот раз неиспользуемые области обрабатываются пользовательским кодом.
Пример
Показывает, как обрабатывать несоединенные области после Mail Merge с помощью пользовательского кода.
// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java | |
// Open the document. | |
Document doc = new Document(dataDir + "TestFile.doc"); | |
// Create a data source which has some data missing. | |
// This will result in some regions that are merged and some that remain after executing mail merge. | |
DataSet data = getDataSource(); | |
// Make sure that we have not set the removal of any unused regions as we will handle them manually. | |
// We achieve this by removing the RemoveUnusedRegions flag from the cleanup options by using the AND and NOT bitwise operators. | |
doc.getMailMerge().setCleanupOptions(doc.getMailMerge().getCleanupOptions() & ~MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS); | |
// Execute mail merge. Some regions will be merged with data, others left unmerged. | |
doc.getMailMerge().executeWithRegions(data); | |
// The regions which contained data now would of been merged. Any regions which had no data and were | |
// not merged will still remain in the document. | |
Document mergedDoc = doc.deepClone(); //ExSkip | |
// Apply logic to each unused region left in the document using the logic set out in the handler. | |
// The handler class must implement the IFieldMergingCallback interface. | |
executeCustomLogicOnEmptyRegions(doc, new EmptyRegionsHandler()); | |
// Save the output document to disk. | |
doc.save(dataDir + "TestFile.CustomLogicEmptyRegions1 Out.doc"); |
Код выполняет различные операции на основе названия региона, полученного с помощью свойства FieldMergingArgs.TableName. Обратите внимание, что в зависимости от вашего документа и регионов вы можете запрограммировать обработчик для запуска логики, зависящей от каждого региона, или кода, который применяется к каждому отдельному региону в документе, или их комбинации.
Логика для области ContactDetails заключается в изменении текста каждого поля в области ContactDetails с соответствующим сообщением об отсутствии данных. Имена каждого поля сопоставляются в обработчике с помощью свойства FieldMergingArgs.FieldName.
Аналогичный процесс применяется к области Suppliers с добавлением дополнительного кода для обработки таблицы, содержащей эту область. Код проверит, содержится ли область в таблице (поскольку она, возможно, уже была удалена). Если это так, то из документа будет удалена вся таблица целиком, а также предшествующий ей абзац, если он отформатирован в стиле заголовка, например “Heading 1”.
Пример
Показывает, как определить пользовательскую логику в обработчике, реализующем IFieldMergingCallback, который выполняется для несвязанных областей в документе.
// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java | |
public static class EmptyRegionsHandler implements IFieldMergingCallback { | |
/** | |
* Called for each field belonging to an unmerged region in the | |
* document. | |
*/ | |
public void fieldMerging(FieldMergingArgs args) throws Exception { | |
// Change the text of each field of the ContactDetails region individually. | |
if ("ContactDetails".equals(args.getTableName())) { | |
// Set the text of the field based off the field name. | |
if ("Name".equals(args.getFieldName())) | |
args.setText("(No details found)"); | |
else if ("Number".equals(args.getFieldName())) | |
args.setText("(N/A)"); | |
} | |
// Remove the entire table of the Suppliers region. Also check if the previous paragraph | |
// before the table is a heading paragraph and if so remove that too. | |
if ("Suppliers".equals(args.getTableName())) { | |
Table table = (Table) args.getField().getStart().getAncestor(NodeType.TABLE); | |
// Check if the table has been removed from the document already. | |
if (table.getParentNode() != null) { | |
// Try to find the paragraph which precedes the table before the table is removed from the document. | |
if (table.getPreviousSibling() != null && table.getPreviousSibling().getNodeType() == NodeType.PARAGRAPH) { | |
Paragraph previousPara = (Paragraph) table.getPreviousSibling(); | |
if (isHeadingParagraph(previousPara)) | |
previousPara.remove(); | |
} | |
table.remove(); | |
} | |
} | |
} | |
/** | |
* Returns true if the paragraph uses any Heading style e.g Heading 1 to | |
* Heading 9 | |
*/ | |
private boolean isHeadingParagraph(Paragraph para) throws Exception { | |
return (para.getParagraphFormat().getStyleIdentifier() >= StyleIdentifier.HEADING_1 && para.getParagraphFormat().getStyleIdentifier() <= StyleIdentifier.HEADING_9); | |
} | |
public void imageFieldMerging(ImageFieldMergingArgs args) throws Exception { | |
// Do Nothing | |
} | |
} |
Результат выполнения приведенного выше кода показан ниже. Не объединенные поля в первой области заменены информативным текстом, а удаление таблицы и заголовка позволяет документу выглядеть завершенным.
Код, который удаляет родительскую таблицу, также можно было бы заставить выполняться в каждом неиспользуемом регионе, а не только в определенном регионе, удалив проверку имени таблицы. В этом случае, если какой-либо регион внутри таблицы не был объединен с какими-либо данными, как регион, так и таблица-контейнер также будут автоматически удалены.
Мы можем вставить другой код в обработчик, чтобы управлять обработкой разделенных областей. Использование приведенного ниже кода в обработчике вместо этого изменит текст в первом абзаце области на полезное сообщение, в то время как все последующие абзацы в области будут удалены. Эти другие абзацы удалены, поскольку они останутся в регионе после объединения нашего сообщения.
Заменяющий текст вводится в первое поле путем установки указанного текста в свойстве FieldMergingArgs.Text. Текст из этого свойства вводится в поле с помощью механизма Mail Merge.
Код применяет это только к первому полю в регионе, проверяя свойство FieldMergingArgs.FieldValue. Значение первого поля в регионе помечается как “FirstField”. Это упрощает реализацию этого типа логики во многих регионах, поскольку не требуется никакого дополнительного кода.
Пример
Показывает, как заменить неиспользуемую область сообщением и удалить лишние абзацы.
// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java | |
// Store the parent paragraph of the current field for easy access. | |
Paragraph parentParagraph = args.getField().getStart().getParentParagraph(); | |
// Define the logic to be used when the ContactDetails region is encountered. | |
// The region is removed and replaced with a single line of text stating that there are no records. | |
if ("ContactDetails".equals(args.getTableName())) { | |
// Called for the first field encountered in a region. This can be used to execute logic on the first field | |
// in the region without needing to hard code the field name. Often the base logic is applied to the first field and | |
// different logic for other fields. The rest of the fields in the region will have a null FieldValue. | |
if ("FirstField".equals(args.getFieldValue())) { | |
FindReplaceOptions opts = new FindReplaceOptions(); | |
opts.setMatchCase(false); | |
opts.setFindWholeWordsOnly(false); | |
// Remove the "Name:" tag from the start of the paragraph | |
parentParagraph.getRange().replace("Name:", "", opts); | |
// Set the text of the first field to display a message stating that there are no records. | |
args.setText("No records to display"); | |
} else { | |
// We have already inserted our message in the paragraph belonging to the first field. The other paragraphs in the region | |
// will still remain so we want to remove these. A check is added to ensure that the paragraph has not already been removed. | |
// which may happen if more than one field is included in a paragraph. | |
if (parentParagraph.getParentNode() != null) | |
parentParagraph.remove(); | |
} | |
} |
Полученный документ после выполнения приведенного выше кода показан ниже. Неиспользуемая область заменяется сообщением о том, что в ней нет записей для отображения.
В качестве другого примера, мы можем вставить приведенный ниже код вместо кода, который изначально обрабатывал SuppliersRegion. Это приведет к отображению сообщения внутри таблицы и объединению ячеек вместо удаления таблицы из документа. Поскольку область находится в таблице с несколькими ячейками, лучше объединить ячейки таблицы вместе и расположить сообщение по центру.
Пример
Показывает, как объединить все родительские ячейки неиспользуемой области и отобразить сообщение в таблице.
// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java | |
// Replace the unused region in the table with a "no records" message and merge all cells into one. | |
if ("Suppliers".equals(args.getTableName())) { | |
if ("FirstField".equals(args.getFieldValue())) { | |
// We will use the first paragraph to display our message. Make it centered within the table. The other fields in other cells | |
// within the table will be merged and won't be displayed so we don't need to do anything else with them. | |
parentParagraph.getParagraphFormat().setAlignment(ParagraphAlignment.CENTER); | |
args.setText("No records to display"); | |
} | |
// Merge the cells of the table together. | |
Cell cell = (Cell) parentParagraph.getAncestor(NodeType.CELL); | |
if (cell != null) { | |
if (cell.isFirstCell()) | |
cell.getCellFormat().setHorizontalMerge(CellMerge.FIRST); // If this cell is the first cell in the table then the merge is started using "CellMerge.First". | |
else | |
cell.getCellFormat().setHorizontalMerge(CellMerge.PREVIOUS); // Otherwise the merge is continued using "CellMerge.Previous". | |
} | |
} |
Результирующий документ после выполнения приведенного выше кода показан ниже.
Наконец, мы можем вызвать метод ExecuteCustomLogicOnEmptyRegions и указать имена таблиц, которые должны обрабатываться в нашем методе-обработчике, указав при этом другие, которые будут автоматически удалены.
Пример
Показывает, как указать только область ContactDetails
, которая будет обрабатываться с помощью класса handler.
// For complete examples and data files, please go to https://github.com/aspose-words/Aspose.Words-for-Java | |
ArrayList<String> regions = new ArrayList<String>(); | |
regions.add("ContactDetails"); | |
executeCustomLogicOnEmptyRegions(doc, new EmptyRegionsHandler(), regions); |
При вызове этой перегрузки с указанным значением ArrayList будет создан источник данных, содержащий только строки данных для указанных областей. Области, отличные от области ContactDetails
, обрабатываться не будут и вместо этого будут автоматически удалены механизмом Mail Merge. Результат приведенного выше вызова с использованием кода из нашего исходного обработчика показан ниже.