MoveToMergeField does not work properly

Hi there,

I have one table sheet on the document and I wanted to display checkbox instead of some values.
But MoveToMergeField function does not work properly. Please see below code sample.

Interesting thing is If I call the function MoveToMergeField two time, it works.

private void Test_MoveToMergeField()
{
#region prepare dataset

        DataSet ds = new DataSet();

        // add category table
        DataTable dt1 = new DataTable();
        dt1.TableName = "category";
        dt1.Columns.Add("category_id", typeof(int));
        dt1.Columns.Add("category_name", typeof(string));
        dt1.Rows.Add(1, "Fruit");
        dt1.Rows.Add(2, "Beverage");
        ds.Tables.Add(dt1);

        // add product table
        DataTable dt2 = new DataTable();
        dt2.TableName = "product";
        dt2.Columns.Add("category_id", typeof(int));
        dt2.Columns.Add("product_id", typeof(int));
        dt2.Columns.Add("product_name", typeof(string));
        dt2.Columns.Add("product_status", typeof(string));
        dt2.Rows.Add(2, 1, "Coca cola", "Active");
        dt2.Rows.Add(2, 2, "Pepsi", "Deactive");
        ds.Tables.Add(dt2);

        // join
        DataColumn[] parentColumns = new DataColumn[] { ds.Tables[0].Columns["category_id"] };
        DataColumn[] childColumns = new DataColumn[] { ds.Tables[1].Columns["category_id"] };
        ds.Relations.Add(parentColumns, childColumns);

        #endregion

        #region generate pdf

        Aspose.Words.Document document = new Aspose.Words.Document("D:\\Aspose_MoveToMergeField.docx");
        Aspose.Words.License lic = new Aspose.Words.License();
        lic.SetLicense("Aspose.Words.lic");

        Aspose.Words.MailMerging.MailMerge m;
        document.MailMerge.FieldMergingCallback = new HandleMergeCheckBox();
        document.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveEmptyParagraphs;
        document.MailMerge.ExecuteWithRegions(ds);
        document.MailMerge.DeleteFields(); // it removes empty MailMerge fields
        document.Save("D:\\Aspose_MoveToMergeField.pdf", SaveFormat.Pdf);

        #endregion
    }

internal class HandleMergeCheckBox : IFieldMergingCallback
{
    void IFieldMergingCallback.FieldMerging(FieldMergingArgs e)
    {
        if (e.FieldName == "product_status")
        {
            DocumentBuilder builder = new DocumentBuilder(e.Document);
            builder.MoveToMergeField(e.FieldName, true, true);
            //builder.MoveToMergeField(e.FieldName, true, true);  // if I call it twice, it works 

            e.Text = null;

            string value = Convert.ToString(e.FieldValue);
            if (value == "Active" || value == "Deactive")
            {
                builder.InsertCheckBox("Active", value == "Active", 0);
                builder.Write(" Active");

                builder.InsertBreak(BreakType.LineBreak);

                builder.InsertCheckBox("Deactive", value == "Deactive", 0);
                builder.Write(" Deactive");
            }
        }
    }

    void IFieldMergingCallback.ImageFieldMerging(ImageFieldMergingArgs e)
    {
    }
}

image.png (5.1 KB)
image.png (14.1 KB)

When I call it twice, it works fine as below.
image.png (13.6 KB)

@Amar33,

To ensure a timely and accurate response, please ZIP and attach the following resources here for testing:

  • Your simplified input Word document
  • Aspose.Words 19.1 generated output DOCX document showing the undesired behavior
  • Your expected document showing the correct output. You can create expected document by using MS Word.

As soon as you get these pieces of information ready, we will start investigation into your issue and provide you more information. Thanks for your cooperation.

Hi awais,

Do you want me to prepare input/output word documents and to test it on the version 19.1 for you?

I spent whole day to investigate and tried to figure out the issue. I think that I provided you enough information how it is working and expected and unexpected results. If you see the pictures things are clear.
So, rest of things are your work guys, because this is your issue not mine.

@Amar33,

Please see these sample input/output documents:
Documents.zip (39.3 KB)

We generated these documents by using the following code:

internal class HandleMergeCheckBox : IFieldMergingCallback
{
    void IFieldMergingCallback.FieldMerging(FieldMergingArgs e)
    {
        if (e.FieldName == "product_status")
        {
            DocumentBuilder builder = new DocumentBuilder(e.Document);
            builder.MoveToMergeField(e.FieldName, true, true);
            //builder.MoveToMergeField(e.FieldName, true, true);  // if I call it twice, it works 

            e.Text = null;

            string value = Convert.ToString(e.FieldValue);
            if (value == "Active" || value == "Deactive")
            {
                builder.InsertCheckBox("Active", value == "Active", 0);
                builder.Write(" Active");

                builder.InsertBreak(BreakType.LineBreak);

                builder.InsertCheckBox("Deactive", value == "Deactive", 0);
                builder.Write(" Deactive");
            }
        }
    }

    void IFieldMergingCallback.ImageFieldMerging(ImageFieldMergingArgs e)
    {
    }
}

DataSet ds = new DataSet();

// add category table
DataTable dt1 = new DataTable();
dt1.TableName = "category";
dt1.Columns.Add("category_id", typeof(int));
dt1.Columns.Add("category_name", typeof(string));
dt1.Rows.Add(1, "Fruit");
dt1.Rows.Add(2, "Beverage");
ds.Tables.Add(dt1);

// add product table
DataTable dt2 = new DataTable();
dt2.TableName = "product";
dt2.Columns.Add("category_id", typeof(int));
dt2.Columns.Add("product_id", typeof(int));
dt2.Columns.Add("product_name", typeof(string));
dt2.Columns.Add("product_status", typeof(string));
dt2.Rows.Add(2, 1, "Coca cola", "Active");
dt2.Rows.Add(2, 2, "Pepsi", "Deactive");
ds.Tables.Add(dt2);

// join
DataColumn[] parentColumns = new DataColumn[] { ds.Tables[0].Columns["category_id"] };
DataColumn[] childColumns = new DataColumn[] { ds.Tables[1].Columns["category_id"] };
ds.Relations.Add(parentColumns, childColumns);
                                          
Document doc = new Document("E:\\temp\\in-189238.docx");

Aspose.Words.MailMerging.MailMerge m;
doc.MailMerge.FieldMergingCallback = new HandleMergeCheckBox();
doc.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveEmptyParagraphs;
doc.MailMerge.ExecuteWithRegions(ds);
doc.MailMerge.DeleteFields(); // it removes empty MailMerge fields

doc.Save("E:\\temp\\19.1.pdf");

Please explain further how is MoveToMergeField not work properly and what is wrong with 19.1-once.pdf. Thanks for your cooperation.

I have a question for you beforehand.

Can you explain me what is difference between calling once

builder.MoveToMergeField(e.FieldName, true, true);

and

calling twice
builder.MoveToMergeField(e.FieldName, true, true);
builder.MoveToMergeField(e.FieldName, true, true);

Why it is resulting 2 different documents?

if you see those documents you will see the differences and you will understand the issue.

Anyways, let me try to make you understand.
If you see the data that is provided in the code, there are 2 tables: category and product.

Category table has 2 rows such as Fruit and Beverage.
Product table has 2 rows (Coca cola and Pepsi) that have the value of category Beverage. There is no record for the category Fruit.
Those 2 tables are joined by their category id.

So that, the output document should be as follows:

  1. it should generate as many sheets as how many records the category table has. Output doc should have 2 sheets for Fruit and Beverage.

  2. 1st sheet (for Fruit) should not have any records. Because there is no products for this category.

  3. 2nd sheet (for Beverage) should have 2 records. Because there are 2 records (coca cola and pepsi) for this category.

  4. “Status” column of the sheets should only contain check boxes instead of plain text such as “Active” or “Deactive”.

Now, if we see the output document 19.1-once.pdf, 2) and 4) are incorrect.
You will see that “Status” column’s check boxes of the first row of the 2nd sheet moved into 1st sheet and it takes plain text “Active” instead of check boxes.

is it clear for you?

@Amar33,

Thanks for the details. We are checking this scenario further on our end and will get back to you soon with more information.

@Amar33,

Please see these input/output documents (Documents.zip (24.3 KB)) and try running the following code to get the desired output:

internal class HandleMergeCheckBox : IFieldMergingCallback
{
    void IFieldMergingCallback.FieldMerging(FieldMergingArgs e)
    {
        if (e.FieldName == "product_status")
        {
            DocumentBuilder builder = new DocumentBuilder(e.Document);
            builder.MoveToMergeField(e.FieldName, true, true);                   

            string value = Convert.ToString(e.FieldValue);
            if (value == "Active" || value == "Deactive")
            {
                builder.InsertCheckBox("Active", value == "Active", 0);
            }

            e.Text = null;
        }
    }

    void IFieldMergingCallback.ImageFieldMerging(ImageFieldMergingArgs e)
    {
    }
} 

DataSet ds = new DataSet();

// add category table
DataTable dt1 = new DataTable();
dt1.TableName = "category";
dt1.Columns.Add("category_id", typeof(int));
dt1.Columns.Add("category_name", typeof(string));
dt1.Rows.Add(1, "Fruit");
dt1.Rows.Add(2, "Beverage");
ds.Tables.Add(dt1);

// add product table
DataTable dt2 = new DataTable();
dt2.TableName = "product";
dt2.Columns.Add("category_id", typeof(int));
dt2.Columns.Add("product_id", typeof(int));
dt2.Columns.Add("product_name", typeof(string));
dt2.Columns.Add("product_status", typeof(string));            

dt2.Rows.Add(2, 1, "Coca cola", "Active");
dt2.Rows.Add(2, 2, "Pepsi", "Deactive");
dt2.Rows.Add(1, 3, "Apple", "Active");

ds.Tables.Add(dt2);

// join
DataColumn[] parentColumns = new DataColumn[] { ds.Tables[0].Columns["category_id"] };
DataColumn[] childColumns = new DataColumn[] { ds.Tables[1].Columns["category_id"] };
ds.Relations.Add(parentColumns, childColumns);

Document doc = new Document("E:\\temp\\in-189238.docx");

doc.MailMerge.FieldMergingCallback = new HandleMergeCheckBox();
doc.MailMerge.ExecuteWithRegions(ds);

doc.Save("E:\\temp\\19.1.pdf");

Hope, this helps.

dt2.Rows.Add(1, 3, “Apple”, “Active”);

Adding a record into the joined table, it works.
But, this is not correct solution to resolve the issue. The bug is still there.
Our case is, all data comes from database depending on business logic and I cannot add or modify the data.

Is it the bug that needs to be resolved or not?

Otherwise, I have the workaround that I found it luckily mentioned earlier.

@Amar33,

We are working on your query and will get back to you soon.

@Amar33,

This is not a bug in Aspose.Words API. Please see these input/output documents (Documents.zip (23.9 KB)) and try running the following code. You just need to replace:

document.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveEmptyParagraphs;

with

doc.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveUnusedRegions;

Complete Code:


DataSet ds = new DataSet();

// add category table
DataTable dt1 = new DataTable();
dt1.TableName = "category";
dt1.Columns.Add("category_id", typeof(int));
dt1.Columns.Add("category_name", typeof(string));
dt1.Rows.Add(1, "Fruit");
dt1.Rows.Add(2, "Beverage");
ds.Tables.Add(dt1);

// add product table
DataTable dt2 = new DataTable();
dt2.TableName = "product";
dt2.Columns.Add("category_id", typeof(int));
dt2.Columns.Add("product_id", typeof(int));
dt2.Columns.Add("product_name", typeof(string));
dt2.Columns.Add("product_status", typeof(string));

dt2.Rows.Add(2, 1, "Coca cola", "Active");
dt2.Rows.Add(2, 2, "Pepsi", "Deactive");

ds.Tables.Add(dt2);

// join
DataColumn[] parentColumns = new DataColumn[] { ds.Tables[0].Columns["category_id"] };
DataColumn[] childColumns = new DataColumn[] { ds.Tables[1].Columns["category_id"] };
ds.Relations.Add(parentColumns, childColumns);

Document doc = new Document("E:\\temp\\in-189238.docx");

doc.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveUnusedRegions;
doc.MailMerge.FieldMergingCallback = new HandleMergeCheckBox();
doc.MailMerge.ExecuteWithRegions(ds);
doc.MailMerge.DeleteFields(); // it removes empty MailMerge fields

doc.Save("E:\\temp\\19.1.pdf");

internal class HandleMergeCheckBox : IFieldMergingCallback
{
    void IFieldMergingCallback.FieldMerging(FieldMergingArgs e)
    {
        if (e.FieldName == "product_status")
        {
            DocumentBuilder builder = new DocumentBuilder(e.Document);
            builder.MoveToMergeField(e.FieldName, true, true);

            string value = Convert.ToString(e.FieldValue);
            if (value == "Active" || value == "Deactive")
            {
                builder.InsertCheckBox("Active", value == "Active", 0);
            }

            e.Text = null;
        }
    }

    void IFieldMergingCallback.ImageFieldMerging(ImageFieldMergingArgs e)
    {
    }
}

Hope, this helps.

Can you explain me

  1. why the checkbox is inserted at wrong location with this code:

document.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveEmptyParagraphs;
builder.MoveToMergeField(e.FieldName, true, true);

  1. why the checkbox is inserted at correct location with this code:

document.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveEmptyParagraphs;
builder.MoveToMergeField(e.FieldName, true, true);
builder.MoveToMergeField(e.FieldName, true, true);

@Amar33,

We are checking this scenario further on our end and will get back to you soon.

@Amar33,

Please check the following code and these output documents: MailMergeCleanupOptions.zip (27.3 KB)

Document doc = new Document(“E:\temp\in-189238.docx”);

//doc.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveUnusedRegions;
doc.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveEmptyParagraphs;
//doc.MailMerge.FieldMergingCallback = new HandleMergeCheckBox();
doc.MailMerge.ExecuteWithRegions(ds);
//doc.MailMerge.DeleteFields(); // it removes empty MailMerge fields

doc.Save("E:\\temp\\19.1-RemoveEmptyParagraphs.docx"); 

In the 19.1-RemoveUnusedRegions.docx, you can see MailMergeCleanupOptions.RemoveUnusedRegions removes the whole row with its content. So, inside IFieldMergingCallback.FieldMerging, a single MoveToMergeField call will move the cursor to the product_status field in second table.

However, as you can see in the ‘19.1-RemoveEmptyParagraphs.docx’, the MailMergeCleanupOptions.RemoveEmptyParagraphs removes empty paragraphs around the first table (but not the mail merge region). This is the behavior of MS Word that Microsoft Word merges two or more consecutive Tables into one big Table if there are no Paragraphs in between them. So, inside IFieldMergingCallback.FieldMerging, a single MoveToMergeField call will move the cursor to the product_status field in first table row. And to insert the value at the correct position, you had to call MoveToMergeField again. (this is Aspose.Words behavior e.g. if Word document contains three merge fields with same name and to be able to insert value only at the third merge field, you will have to call MoveToMergeField method three times.)

So, this is the expected behavior. If we can help you with anything, please feel free to ask.

Please also refer to: How to Remove Unmerged Fields, Empty Paragraphs and Unmerged Regions