Como aplicar lógica personalizada a regiões não mescladas
Existem algumas situações em que a remoção completa de regiões não mescladas do documento durante Mail Merge não é desejada ou faz com que o documento pareça incompleto. Isso pode ocorrer quando a ausência de dados de entrada deve ser exibida ao usuário na forma de uma mensagem, em vez de a região ser completamente removida.
Há também momentos em que a remoção da região não utilizada por si só não é suficiente, por exemplo, se a região for precedida de um título ou se a região estiver contida num quadro. Se esta região não for utilizada, o Título e a tabela continuarão a permanecer após a remoção da região, o que parecerá deslocado no documento.
Este artigo fornece uma solução para definir manualmente como as regiões não utilizadas no documento são tratadas. O código base para esta funcionalidade é fornecido e pode ser facilmente reutilizado noutro projecto.
A lógica a ser aplicada a cada região é definida dentro de uma classe que implementa a interface IFieldMergingCallback. Da mesma forma, um manipulador Mail Merge pode ser configurado para controlar como cada campo é mesclado, este manipulador pode ser configurado para executar ações em cada campo em uma região não utilizada ou na região como um todo. Dentro deste manipulador, você pode definir o código para alterar o texto de uma região, remover nós ou linhas e células vazias, etc.
Nesta amostra, utilizaremos o documento apresentado abaixo. Contém regiões aninhadas e uma região contida numa tabela.
Como uma demonstração rápida, podemos executar um banco de dados de amostra no documento de amostra com o sinalizador MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS habilitado. Esta propriedade removerá automaticamente regiões não mescladas do documento durante um mail merge.
A fonte de dados inclui dois registos para a Região StoreDetails, mas propositadamente tem quaisquer dados para as regiões ContactDetails filhas para um dos registos. Além disso, a Região Suppliers também não possui linhas de dados. Isso fará com que as regiões não utilizadas permaneçam no documento. O resultado após a fusão do documento com esta fonte de dados está abaixo.
Conforme observado na imagem, você pode ver que a região ContactDetails para o segundo registro e as regiões Suppliers foram removidas automaticamente pelo mecanismo Mail Merge, pois não possuem dados. No entanto, existem alguns problemas que fazem com que este documento de saída pareça incompleto:
- A região ContactDetails ainda deixa um parágrafo com o texto"dados de contacto".
- No mesmo caso, não há indicação de que não existam números de telefone, apenas um espaço em branco que possa causar confusão.
- A tabela e o título relacionados à região Suppliers também permanecem após a remoção da região dentro da tabela.
A técnica fornecida neste artigo demonstra como aplicar a lógica personalizada a cada região não mesclada para evitar esses problemas.
A Solução
Para aplicar manualmente a lógica a cada região não utilizada no documento, aproveitamos os recursos já disponíveis em Aspose.Words.
O mecanismo Mail Merge fornece uma propriedade para remover regiões não utilizadas por meio do sinalizador MailMergeCleanupOptions.RemoveUnusedRegions. Isso pode ser desativado para que essas regiões sejam deixadas intactas durante um mail merge. Isso nos permite deixar as regiões não mescladas no documento e, em vez disso, tratá-las manualmente.
Podemos então tirar proveito da propriedade MailMerge.FieldMergingCallback como um meio de aplicar nossa própria lógica personalizada a essas regiões não mescladas durante Mail Merge através do uso de uma classe de manipulador implementando a interface IFieldMergingCallback.
Este código dentro da classe handler é a única classe que você precisará modificar para controlar a lógica aplicada a regiões não mescladas. O outro código desta amostra pode ser reutilizado sem modificação em qualquer projecto.
Este exemplo de projecto demonstra esta técnica. Envolve as seguintes etapas:
- Execute Mail Merge no documento usando sua fonte de dados. O sinalizador MailMergeCleanupOptions.RemoveUnusedRegions está desativado por enquanto queremos que as regiões permaneçam para que possamos lidar com elas manualmente. Quaisquer regiões sem dados serão deixadas sem mistura no documento.
- Chame o método ExecuteCustomLogicOnEmptyRegions. Este método é fornecido nesta amostra. Ele executa ações que permitem que o manipulador especificado seja chamado para cada região não mesclada. Este método é reutilizável e pode ser copiado inalterado para qualquer projecto que o exija (juntamente com quaisquer métodos dependentes).Este método executa as seguintes etapas:
- Define o manipulador especificado pelo Usuário para a propriedade MailMerge.FieldMergingCallback.
- Chama o método CreateDataSourceFromDocumentRegions que aceita os nomes das regiões que contêm Document e ArrayList do utilizador. Este método criará uma fonte de dados fictícia contendo tabelas para cada região não mesclada no documento.
- Executa Mail Merge no documento utilizando a fonte de dados fictícia. Quando Mail Merge é executado com esta fonte de dados, ele permite que o manipulador especificado pelo usuário seja chamado para cada região unmerge e a lógica personalizada aplicada
O Código
A aplicação do método ExecuteCustomLogicOnEmptyRegions encontra-se abaixo. Este método aceita vários parâmetros:
- O objeto Document contendo regiões não mescladas que devem ser tratadas pelo manipulador passado.
- A classe do manipulador que define a lógica a ser aplicada a regiões não mescladas. Este manipulador deve implementar o IFieldMergingCallback interface.
- Através do uso da sobrecarga apropriada, o método também pode aceitar um terceiro parâmetro – uma lista de nomes de regiões como strings. Se isto for especificado, apenas os nomes das regiões restantes do documento especificado na lista serão tratados manualmente. Outras regiões que são encontradas não serão chamadas pelo manipulador e removidas automaticamente. Quando a sobrecarga com apenas dois parâmetros é especificada, todas as regiões restantes do documento são incluídas pelo método a ser tratado manualmente.
Exemplo
Mostra como executar lógica personalizada em regiões não utilizadas usando o manipulador especificado.
// 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(); | |
} |
Exemplo
Define o método utilizado para tratar manualmente regiões não mescladas.
// 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; | |
} |
Este método envolve encontrar todas as regiões não mescladas no documento. Isso é feito usando o método MailMerge.GetFieldNames. Este método retorna todos os campos de mesclagem no documento, incluindo os marcadores de início e fim da região (representados por campos de mesclagem com o prefixo TableStart ou TableEnd).
Quando um campo de mesclagem TableStart
é encontrado, ele é adicionado como um novo DataTable ao DataSet. Como uma região pode aparecer mais de uma vez (por exemplo, porque é uma região aninhada onde a região pai foi mesclada com vários registros), a tabela só é criada e adicionada se ainda não existir no DataSet.
Quando um início de região apropriado é encontrado e adicionado à base de dados, o campo seguinte (que corresponde ao primeiro campo da região) é adicionado ao DataTable. Apenas o primeiro campo deve ser adicionado para cada campo na região a ser mesclado e passado para o manipulador.
Também definimos o valor do campo do primeiro campo como “FirstField” para facilitar a aplicação da lógica ao primeiro ou a outros campos da região. Ao incluir isso, significa que não é necessário codificar o nome do primeiro campo ou implementar código extra para verificar se o campo atual é o primeiro no código do manipulador.
O código abaixo demonstra como este sistema funciona. O documento apresentado no início deste artigo é remergido com a mesma fonte de dados, mas desta vez, as regiões não utilizadas são tratadas por código personalizado.
Exemplo
Mostra como lidar com regiões não mescladas após Mail Merge com código definido pelo Usuário.
// 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"); |
O código executa operações diferentes com base no nome da região recuperada usando a propriedade FieldMergingArgs.TableName. Observe que, dependendo do documento e das Regiões, você pode codificar o manipulador para executar a lógica dependente de cada região ou código que se aplica a cada região não mesclada no documento ou uma combinação de ambos.
A lógica para a Região ContactDetails envolve alterar o texto de cada campo na Região ContactDetails com uma mensagem apropriada informando que não há dados. Os nomes de cada campo são correspondidos dentro do manipulador usando a propriedade FieldMergingArgs.FieldName.
Um processo semelhante é aplicado à região Suppliers com a adição de código extra para lidar com a tabela que contém a região. O código verificará se a região está contida numa tabela (uma vez que pode já ter sido removida). Se for, removerá toda a tabela do documento, bem como o parágrafo que o precede, desde que esteja formatado com um estilo de cabeçalho, por exemplo, “Heading 1”.
Exemplo
Mostra como definir a lógica personalizada em um manipulador implementando IFieldMergingCallback que é executado para regiões não mescladas no documento.
// 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 | |
} | |
} |
O resultado do código acima é mostrado abaixo. Os campos não misturados na primeira região são substituídos por texto informativo e a remoção da tabela e do título permite que o documento pareça completo.
O código que remove a tabela pai também pode ser executado em todas as regiões não utilizadas, em vez de apenas em uma região específica, removendo a verificação do nome da tabela. Nesse caso, se qualquer região dentro de uma tabela não foi mesclada com nenhum dado, tanto a região quanto a tabela de contêiner também serão removidas automaticamente.
Podemos inserir código diferente no manipulador para controlar como as regiões não mescladas são tratadas. Usar o código abaixo no manipulador, em vez disso, mudará o texto no primeiro parágrafo da região para uma mensagem útil, enquanto quaisquer parágrafos subsequentes na região são removidos. Estes outros parágrafos são removidos, uma vez que permaneceriam na região após a fusão da nossa mensagem.
O texto de substituição é mesclado no primeiro campo definindo o texto especificado na propriedade FieldMergingArgs.Text. O texto desta propriedade é mesclado no campo pelo mecanismo Mail Merge.
O código aplica isso apenas ao primeiro campo da região, verificando a propriedade FieldMergingArgs.FieldValue. O valor do campo do primeiro campo da região está marcado com " FirstField". Isso torna esse tipo de lógica mais fácil de implementar em muitas regiões, pois nenhum código extra é necessário.
Exemplo
Mostra como substituir uma região não utilizada por uma mensagem e remover parágrafos adicionais.
// 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(); | |
} | |
} |
O documento resultante após a execução do código acima é mostrado abaixo. A região não utilizada é substituída por uma mensagem a indicar que não existem registos a apresentar.
Como outro exemplo, podemos inserir o código abaixo no lugar do código que originalmente manipula o SuppliersRegion. Isso exibirá uma mensagem dentro da tabela e mesclará as células em vez de remover a tabela do documento. Como a região reside em uma tabela com várias células, parece melhor ter as células da tabela mescladas e a mensagem centralizada.
Exemplo
Mostra como mesclar todas as células-mãe de uma região não utilizada e exibir uma mensagem na tabela.
// 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". | |
} | |
} |
O documento resultante após a execução do código acima é mostrado abaixo.
Finalmente, podemos chamar o método ExecuteCustomLogicOnEmptyRegions e especificar os nomes das tabelas que devem ser manipulados dentro do nosso método handler, enquanto especificamos outros a serem removidos automaticamente.
Exemplo
Mostra como especificar apenas a região ContactDetails
a ser tratada através da classe 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); |
Chamar essa sobrecarga com o ArrayList especificado criará a fonte de dados que contém apenas linhas de dados para as regiões especificadas. Regiões diferentes da região ContactDetails
não serão tratadas e serão removidas automaticamente pelo mecanismo Mail Merge. O resultado da chamada acima usando o código em nosso manipulador original é mostrado abaixo.