Cómo Aplicar Lógica Personalizada a Regiones No Fusionadas
Hay algunas situaciones en las que no se desea eliminar por completo las regiones no fusionadas del documento durante Mail Merge o el documento parece incompleto. Esto puede ocurrir cuando la ausencia de datos de entrada se debe mostrar al usuario en forma de mensaje en lugar de eliminar completamente la región.
También hay ocasiones en las que eliminar la región no utilizada por sí sola no es suficiente, por ejemplo, si la región está precedida por un título o si la región está contenida en una tabla. Si esta región no se usa, el título y la tabla permanecerán después de eliminar la región, lo que se verá fuera de lugar en el documento.
Este artículo proporciona una solución para definir manualmente cómo se manejan las regiones no utilizadas en el documento. Se proporciona el código base para esta funcionalidad y se puede reutilizar fácilmente en otro proyecto.
La lógica que se aplicará a cada región se define dentro de una clase que implementa la interfaz IFieldMergingCallback. De la misma manera, se puede configurar un controlador Mail Merge para controlar cómo se fusiona cada campo, este controlador se puede configurar para realizar acciones en cada campo en una región no utilizada o en la región en su conjunto. Dentro de este controlador, puede configurar el código para cambiar el texto de una región, eliminar nodos o filas y celdas vacías, etc.
En esta muestra, usaremos el documento que se muestra a continuación. Contiene regiones anidadas y una región contenida dentro de una tabla.
Como demostración rápida, podemos ejecutar una base de datos de muestra en el documento de muestra con el indicador MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS habilitado. Esta propiedad eliminará automáticamente las regiones no fusionadas del documento durante un mail merge.
La fuente de datos incluye dos registros para la región StoreDetails, pero intencionalmente tiene datos para las regiones secundarias ContactDetails de uno de los registros. Además, la región Suppliers tampoco tiene filas de datos. Esto hará que las regiones no utilizadas permanezcan en el documento. El resultado después de fusionar el documento con esta fuente de datos se muestra a continuación.
Como se indica en la imagen, puede ver que la región ContactDetails para el segundo registro y las regiones Suppliers han sido eliminadas automáticamente por el motor Mail Merge ya que no tienen datos. Sin embargo, hay algunos problemas que hacen que este documento de salida parezca incompleto:
- La región ContactDetails aún deja un párrafo con el texto “Detalles de contacto”.
- En el mismo caso, no hay indicios de que no haya números de teléfono, solo un espacio en blanco que podría generar confusión.
- La tabla y el título relacionados con la región Suppliers también permanecen después de eliminar la región dentro de la tabla.
La técnica proporcionada en este artículo demuestra cómo aplicar lógica personalizada a cada región no fusionada para evitar estos problemas.
La Solución
Para aplicar lógica manualmente a cada región no utilizada del documento, aprovechamos las funciones que ya están disponibles en Aspose.Words.
El motor Mail Merge proporciona una propiedad para eliminar regiones no utilizadas a través del indicador MailMergeCleanupOptions.RemoveUnusedRegions. Esto se puede deshabilitar para que dichas regiones no se toquen durante un mail merge. Esto nos permite dejar las regiones no fusionadas en el documento y manejarlas manualmente nosotros mismos.
Luego podemos aprovechar la propiedad MailMerge.FieldMergingCallback como un medio para aplicar nuestra propia lógica personalizada a estas regiones no fusionadas durante Mail Merge mediante el uso de una clase de controlador que implemente la interfaz IFieldMergingCallback.
Este código dentro de la clase handler es la única clase que necesitará modificar para controlar la lógica aplicada a las regiones no fusionadas. El otro código de este ejemplo se puede reutilizar sin modificaciones en ningún proyecto.
Este proyecto de muestra demuestra esta técnica. Implica los siguientes pasos:
- Ejecute Mail Merge en el documento utilizando su fuente de datos. La bandera MailMergeCleanupOptions.RemoveUnusedRegions está deshabilitada por ahora, queremos que las regiones permanezcan para que podamos manejarlas manualmente. Las regiones sin datos se dejarán sin fusionar en el documento.
- Llame al método ExecuteCustomLogicOnEmptyRegions. Este método se proporciona en esta muestra. Realiza acciones que permiten llamar al controlador especificado para cada región no fusionada. Este método es reutilizable y se puede copiar inalterado en cualquier proyecto que lo requiera (junto con los métodos dependientes).Este método ejecuta los siguientes pasos:
- Establece el controlador especificado por el usuario en la propiedad MailMerge.FieldMergingCallback.
- Llama al método CreateDataSourceFromDocumentRegions que acepta los nombres de las regiones que contienen Document y ArrayList del usuario. Este método creará una fuente de datos ficticia que contendrá tablas para cada región no fusionada del documento.
- Ejecuta Mail Merge en el documento utilizando la fuente de datos ficticia. Cuando se ejecuta Mail Merge con esta fuente de datos, permite llamar al controlador especificado por el usuario para cada región sin fusionar y aplicar la lógica personalizada
El Código
La implementación del método ExecuteCustomLogicOnEmptyRegions se encuentra a continuación. Este método acepta varios parámetros:
- El objeto Document que contiene regiones no fusionadas que deben ser manejadas por el manejador pasado.
- La clase handler que define la lógica que se aplicará a las regiones no fusionadas. Este controlador debe implementar el IFieldMergingCallback interfaz.
- Mediante el uso de la sobrecarga adecuada, el método también puede aceptar un tercer parámetro: una lista de nombres de región como cadenas. Si se especifica esto, solo se manejarán manualmente los nombres de región que queden en el documento especificado en la lista. El controlador no llamará a otras regiones que se encuentren y las eliminará automáticamente. Cuando se especifica la sobrecarga con solo dos parámetros, cada región restante en el documento se incluye mediante el método que se manejará manualmente.
Ejemplo
Muestra cómo ejecutar lógica personalizada en regiones no utilizadas utilizando el controlador 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(); | |
} |
Ejemplo
Define el método utilizado para manejar manualmente regiones no fusionadas.
// 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 implica encontrar todas las regiones no fusionadas en el documento. Esto se logra utilizando el método MailMerge.GetFieldNames. Este método devuelve todos los campos de combinación del documento, incluidos los marcadores de inicio y finalización de la región (representados por campos de combinación con el prefijo TableStart o TableEnd).
Cuando se encuentra un campo de combinación TableStart
, se agrega como un nuevo DataTable al DataSet. Dado que una región puede aparecer más de una vez (por ejemplo, porque es una región anidada donde la región principal se ha fusionado con varios registros), la tabla solo se crea y agrega si aún no existe en DataSet.
Cuando se encuentra un inicio de región apropiado y se agrega a la base de datos, el siguiente campo (que corresponde al primer campo de la región) se agrega al DataTable. Solo se requiere agregar el primer campo para cada campo en la región que se fusionará y se pasará al controlador.
También establecemos el valor del campo del primer campo en “FirstField " para facilitar la aplicación de la lógica al primero u otros campos de la región. Al incluir esto, significa que no es necesario codificar el nombre del primer campo o implementar código adicional para verificar si el campo actual es el primero en el código del controlador.
El siguiente código demuestra cómo funciona este sistema. El documento que se muestra al principio de este artículo vuelve a aparecer con la misma fuente de datos, pero esta vez, las regiones no utilizadas se manejan mediante código personalizado.
Ejemplo
Muestra cómo manejar regiones no fusionadas después de Mail Merge con código definido por el usuario.
// 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"); |
El código realiza diferentes operaciones en función del nombre de la región recuperada mediante la propiedad FieldMergingArgs.TableName. Tenga en cuenta que, dependiendo de su documento y regiones, puede codificar el controlador para que ejecute lógica dependiente de cada región o código que se aplique a cada región no fusionada en el documento o una combinación de ambos.
La lógica para la región ContactDetails implica cambiar el texto de cada campo en la región ContactDetails con un mensaje apropiado que indique que no hay datos. Los nombres de cada campo se comparan dentro del controlador utilizando la propiedad FieldMergingArgs.FieldName.
Se aplica un proceso similar a la región Suppliers con la adición de código adicional para manejar la tabla que contiene la región. El código verificará si la región está contenida dentro de una tabla (ya que es posible que ya se haya eliminado). Si es así, eliminará toda la tabla del documento, así como el párrafo que la precede, siempre que esté formateada con un estilo de encabezado, por ejemplo, “Heading 1”.
Ejemplo
Muestra cómo definir lógica personalizada en un controlador que implementa IFieldMergingCallback que se ejecuta para regiones no fusionadas en el 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 | |
} | |
} |
El resultado del código anterior se muestra a continuación. Los campos sin combinar dentro de la primera región se reemplazan con texto informativo y la eliminación de la tabla y el encabezado permite que el documento parezca completo.
El código que elimina la tabla principal también podría ejecutarse en cada región no utilizada en lugar de solo en una región específica eliminando la verificación del nombre de la tabla. En este caso, si alguna región dentro de una tabla no se fusionó con ningún dato, tanto la región como la tabla contenedora también se eliminarán automáticamente.
Podemos insertar código diferente en el controlador para controlar cómo se manejan las regiones no fusionadas. En su lugar, usar el código a continuación en el controlador cambiará el texto en el primer párrafo de la región a un mensaje útil, mientras que se eliminarán los párrafos posteriores de la región. Estos otros párrafos se eliminan ya que permanecerían en la región después de fusionar nuestro mensaje.
El texto de reemplazo se combina en el primer campo estableciendo el texto especificado en la propiedad FieldMergingArgs.Text. El motor Mail Merge fusiona el texto de esta propiedad en el campo.
El código aplica esto solo para el primer campo de la región marcando la propiedad FieldMergingArgs.FieldValue. El valor de campo del primer campo de la región está marcado con " FirstField”. Esto hace que este tipo de lógica sea más fácil de implementar en muchas regiones, ya que no se requiere código adicional.
Ejemplo
Muestra cómo reemplazar una región no utilizada con un mensaje y eliminar párrafos adicionales.
// 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(); | |
} | |
} |
El documento resultante después de que se haya ejecutado el código anterior se muestra a continuación. La región no utilizada se reemplaza con un mensaje que indica que no hay registros para mostrar.
Como otro ejemplo, podemos insertar el código a continuación en lugar del código que originalmente manejaba el SuppliersRegion. Esto mostrará un mensaje dentro de la tabla y fusionará las celdas en lugar de eliminar la tabla del documento. Dado que la región reside dentro de una tabla con varias celdas, parece mejor fusionar las celdas de la tabla y centrar el mensaje.
Ejemplo
Muestra cómo fusionar todas las celdas principales de una región no utilizada y mostrar un mensaje dentro de la tabla.
// 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". | |
} | |
} |
El documento resultante después de que se haya ejecutado el código anterior se muestra a continuación.
Finalmente, podemos llamar al método ExecuteCustomLogicOnEmptyRegions y especificar los nombres de tabla que deben manejarse dentro de nuestro método handler, mientras especificamos otros que se eliminarán automáticamente.
Ejemplo
Muestra cómo especificar solo la región ContactDetails
que se manejará a través de la clase 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); |
Llamar a esta sobrecarga con el ArrayList especificado creará la fuente de datos que solo contiene filas de datos para las regiones especificadas. Las regiones que no sean la región ContactDetails
no se manejarán y, en cambio, el motor Mail Merge las eliminará automáticamente. El resultado de la llamada anterior usando el código en nuestro controlador original se muestra a continuación.