Aspose .NET word mail merge with multiple tables

I have a C# application using Aspose to execute a mail merge. I have a datasource set up with 2 tables. One table is called MasterPetition and the other table is called MasterPetitionFiles. I execute this code to create the relationshipt:

dsMasterPetition.Tables[0].TableName = "MasterPetition";
dsMasterPetition.Tables[1].TableName = "MasterPetitionFile";
dsMasterPetition.Relations.Add(dsMasterPetition.Tables[1].Columns["FileNum"], dsMasterPetition.Tables[0].Columns["FileNum"]);

In my word template, I have <TableStart:MasterPetitionFile> and <TableEnd:MasterPetitionFile> as the parent table and <TableStart:MasterPetition> and <TableEnd:MasterPetition> as the child table.

The output that gets generates contains 1 record from the parent table and then multiple records from the child table. This is the expected behavior.

What I would like to know is if there is an instruction that would instruct ASPOSE to generate the parent record data for each child record that gets generated.

In other words, this is what I produce now:

Parent Data record 1
    Child Data  record 1
    Child Data record 2
    Child Data record 3
Parent Data record 2
    Child Data record 4
    Child Data record 5

This is what I want:

Parent Data record 1
    Child Data record 1
Parent Data record 1
    Child Data record 2
Parent Data record 1
    Child Data record 3
Parent Data record 2
    Child Data record 4
Parent Data record 2
    Child Data record 5

I have attached a picture of the table I have set up in my word document.

And insight on how I can accomplish this?
Thank you.

@jonathn6 What you are getting is an expected output, since in your data source Parent Data record 1 record has 3 corresponding child records.
I am afraid there is no direct way to achieve what you need by configuring the template. To get the expected output I would suggest to create one region in your template and then create data view or data table that includes both fields from parent and child tables.

I was honestly thing of that. Thank you.

1 Like

@alexey.noskov - I have been unable to get this to work properly. Would you be kind enough to take a look at 2 attachments. One is the word document that I am using as the mail merge template. The second is a .pdf file that was generated. I marked up the .pdf document to show what data is coming from the parent record and what data comes from the child record. There’s got to be a way for each record that gets displayed to contain the filenum and patitioner data. I just haven’t been able to come up with the right combination. Would you mind taking a look? Thank you so much.
MasterPetition.docx (22.0 KB)

sample.pdf (126.0 KB)

@jonathn6 If possible, could you please also attach your sample data source? You can replace real data with dummy values and save DataSet as XML, so we can test with your data on our side. We will check the issue and provide you more information.
As I can see from your output document, the output is the same as described in your initial post, i.e. there are two child records under one parent record.

@alexey.noskov : I am attaching a spreadsheet with sample data. This is a snippet of code that runs in my application:

DataSet dsMasterPetition = GetMasterPetitionKML(FileNum.ToString(), TaxYear, RespondentKey, Suffix);
if (dsMasterPetition.Tables[0].Rows.Count > 0)
{
    WordViewModel_MasterPetition MailMergeData = new WordViewModel_MasterPetition();
    MailMergeData = GetMasterPetitionHeaderData(FileNum, Suffix, RespondentKey, TaxYear, IndexNo, ReturnDate, PetitionDate);
    dsMasterPetition.Tables[0].TableName = "MasterPetition";
    dsMasterPetition.Tables[1].TableName = "MasterPetitionFile";
    dsMasterPetition.Relations.Add(dsMasterPetition.Tables[1].Columns["FileNum"], dsMasterPetition.Tables[0].Columns["FileNum"]);

    string masterFilePath = GenericHelper.FetchLetterTemplates("MasterPetition");
    MailMergeData.CaptionRespondent = "THE ASSESSOR and THE BOARD OF ASSESSMENT REVIEW OF " + MailMergeData.CaptionRespondent;
    try
    {

        object oMPQRCode = dsMasterPetition.Tables[0].Rows[0].ItemArray[27];
        string FileAndSuffix = "";
        FileAndSuffix = FileNum > 0 ? "FILE: " + FileNum.ToString() + "-" + Suffix : "";
        Aspose.Words.Document masterTemplate = new Aspose.Words.Document(masterFilePath);

        masterTemplate.FieldOptions.BarcodeGenerator = new CustomBarcodeGenerator();
        masterTemplate.MailMerge.CleanupOptions = Aspose.Words.MailMerging.MailMergeCleanupOptions.RemoveEmptyParagraphs;
        masterTemplate.MailMerge.ExecuteWithRegions(dsMasterPetition);

The dsMasterPetition calls a function called GetMasterPetitionKML. This is the code that exeutes in GetMasterPetitionKML:

public DataSet GetMasterPetitionKML(string fileNum, string taxYear, int Respondent, string Suffix)
{
    string sConnectionString = ConfigurationManager.ConnectionStrings["TaxCertConnectionString"].ToString();
    DataSet ds = new DataSet();
    string sFirmName = GenericHelper.GetFirmName();

    using (SqlConnection conn = new SqlConnection(sConnectionString))
    {

        if (Respondent > 0)
        {

            using (SqlCommand scCommand = new SqlCommand("p_get_Master_Petition_ByRespKML", conn))
            {
                using (SqlDataAdapter da = new SqlDataAdapter(scCommand))
                {
                    scCommand.CommandType = CommandType.StoredProcedure;
                    scCommand.Parameters.AddWithValue("@RespondentKey", Respondent);
                    scCommand.Parameters.AddWithValue("@tax_year", taxYear);
                    scCommand.Parameters.AddWithValue("@barcode_ty", "MasterPetition");
                    scCommand.Parameters.AddWithValue("@user", HttpContext.Current.User.Identity.Name);
                    scCommand.Parameters.AddWithValue("@firmName", sFirmName);
                    try
                    {
                        da.Fill(ds);

                    }
                    catch (Exception ex)
                    {

                        throw;
                    }
                }
            }
        }
        else
        {
            using (SqlCommand scCommand = new SqlCommand("p_get_Master_PetitionKML", conn))
            {
                using (SqlDataAdapter da = new SqlDataAdapter(scCommand))
                {
                    scCommand.CommandType = CommandType.StoredProcedure;
                    scCommand.Parameters.AddWithValue("@FileNum", fileNum);
                    scCommand.Parameters.AddWithValue("@tax_year", taxYear);
                    scCommand.Parameters.AddWithValue("@barcode_ty", "TestMasterPetition");
                    scCommand.Parameters.AddWithValue("@user", HttpContext.Current.User.Identity.Name);
                    scCommand.Parameters.AddWithValue("@firmName", sFirmName);
                    scCommand.Parameters.AddWithValue("@Suffix", Suffix);
                    da.Fill(ds);
                }
            }
        }

    }
    return ds;
}

In my senario, Respondent = 78. The stored procedure returns 2 tables of data. The data that was returned is what you will find in the spreadsheet.

If there is anything else you need, please let me know.

Again, thank you for your assisstance.
ASPOSE sample data.zip (14.5 KB)

@alexey.noskov - I am also attaching an xml file of the data that is returned from the stored procedure if that is easier for you to work with.
xml_data.zip (1.6 KB)

@jonathn6 You can join the data into one table and use one region in your template. For example see the following code, data, template and produced output:

DataSet ds = new DataSet();
ds.ReadXml(@"C:\Temp\data.xml");
DataTable MasterPetition = ds.Tables["Table1"];
DataTable MasterPetitionFile = ds.Tables["Table2"];

// join the tables
DataTable jointData = JoinDataTable(MasterPetitionFile, MasterPetition, "FileNum");
jointData.TableName = "JointData";

// Debug code the print the created table structure.
foreach (DataColumn c in jointData.Columns)
    Console.Write(c.ColumnName + "\t\t");
Console.WriteLine();
foreach (DataRow r in jointData.Rows)
{
    foreach (DataColumn c in jointData.Columns)
        Console.Write(r[c] + "\t\t");
    Console.WriteLine();
}

// open tempplate and execute mail merge.
Document doc = new Document(@"C:\Temp\in.docx");
doc.MailMerge.ExecuteWithRegions(jointData);
doc.Save(@"C:\Temp\out.docx");
public static DataTable JoinDataTable(DataTable dataTable1, DataTable dataTable2, string joinField)
{
    DataTable dt = new DataTable();
    var joinTable = from t1 in dataTable1.AsEnumerable()
                    join t2 in dataTable2.AsEnumerable()
                        on t1[joinField] equals t2[joinField]
                    select new { t1, t2 };

    foreach (DataColumn col in dataTable1.Columns)
    {
        if (dt.Columns[col.ColumnName] == null)
            dt.Columns.Add(col.ColumnName, col.DataType);
    }
    foreach (DataColumn col in dataTable2.Columns)
    {
        if (dt.Columns[col.ColumnName] == null)
            dt.Columns.Add(col.ColumnName, col.DataType);
    }

    foreach (var row in joinTable)
        dt.Rows.Add(row.t1.ItemArray.Union(row.t2.ItemArray).ToArray());

    return dt;
}

data.zip (1.5 KB)
in.docx (21.8 KB)
out.docx (19.2 KB)

@alexey.noskov - Thank you for taking the time to look into this. I noticed that in the resulting document, there is a blank page at the start of the document. I’m not sure why that would be. In addition, page 2 contains the table heading but no table data and all the following pages contain table data but no heading. Also, in the pages with data, the data inside the cells is no longer being generated correctly. I will try to debug what you have provided.

In addition, when I try to run your code with the data being returned by the stored procedure (not the xml text file), the application fails on this line in the join function:

dt.Rows.Add(row.t1.ItemArray.Union(row.t2.ItemArray).ToArray());

The error states that the data cannot be converted into an Int32. I believe it is working on the first row of data and the value would be 53000.0000.

@jonathn6

  1. The only thing I have changed in your original template is regions structure. Here is the modified template that does not produce an empty page (there was a mess with Keep With Next property in the paragraphs):
    in.docx (21.8 KB)
    out.docx (19.2 KB)

  2. The provided code is not a ready to use solution, it only demonstrates the basic technique. In your case since you are using stored procedure to get data, you can use INNER JOIN in your SQL request and return already joint table with required fields.

@alexey.noskov - I am finally able to make use of your suggestions, modified them slightly, and I am now producing the document as expected, except for 1 issue. There are instances where data is being generated on across pages which should not be. I have tried using various combinations of the Keep With Next Property but I can’t seem to land on the proper combination.

I have attached a zipped file which contains .xlm files that I generated out of the application, the new word mail merge template, the code that is running to generate the output document, and the output document. I have indicated the lines in the resulting document that are being generated at the end of the table on a page where it should really be at the start of the table on the next page.

Is this something that you would be able to help me with?

Thank you!
files for aspose.zip (142.0 KB)

@jonathn6 The problem occurs because there is a nested table. Please try using separate tables and MailMergeCleanupOptions.RemoveEmptyParagraphs option to remove empty paragraph between them after executing mail merge. For example see the following modified template and code:

DataSet ds = new DataSet();
ds.ReadXml(@"C:\Temp\jointData_data.xml");

Document doc = new Document(@"C:\Temp\in.docx");
doc.MailMerge.CleanupOptions = MailMergeCleanupOptions.RemoveEmptyParagraphs;
doc.MailMerge.ExecuteWithRegions(ds);
doc.Save(@"C:\Temp\out.docx");

in.docx (49.8 KB)
out.docx (39.8 KB)

@alexey.noskov - I am now generating the document correctly. I did not change my incoming data. I do not see any difference between my original template and the template you provided as the in.docx. Is the only difference between your version and my version the removeemptyparagraphs? I’m trying to understand this process and why yours worked and mine did not. Thank you.

@jonathn6 There is difference between templates. In the modified template I have attached in the previous post I have remove the outer table. So there are no nested tables anymore. Here is structure of the tables in your original template:

Here is structure in the modified template: