Mail merge with Aspose

Costas

Administrator
Staff member
Primarily needed something extra from what WORD offers. Add a complete datatable to a document, among with simple fields.

1)
Create a new document

2)
No need to connect any datasource. Add a simple field by going to Insert > Quick Parts > Field (fieldname is case insensitive)

7o32Thw.jpg


choose MergeField and write the 'variable' name

5r0pDPs.png


this will be appear on document as (gray when cursor is on it)

xYd41Cs.png
the only possible way to delete this field is when is gray, press CTRL + SHIFT + F9, once this combination pressed the MergeField will be removed and will be common text.



Imagine now that you have 10 spare simple Merge fields, at code level, you feed them as :

C#:
string[] fieldNames = {
    "RecipientName", "SenderName", "FaxNumber", "PhoneNumber",
    "Subject", "Body", "Urgent", "ForReview", "PleaseComment"
};

object[] fieldValues = {
    "Josh", "Jenny", "123456789", "", "Hello",
    "<b>HTML Body Test message 1</b>", true, false, true
};

//simple Execute is for simple fields
doc.MailMerge.Execute(fieldNames, fieldValues);

find more here.

3)
On the same way we doing for the datatable, but to signalize aspose is a table we have to wrap the area with TableStart:Orders TableEnd:Orders (remember no spaces), both are Merge fields (create them as step 2). (fieldname is case insensitive also for tablename)

when we have this Northwind.mdb table
6DvCPjc.png

we create this document (all as Merge fields)
f8jqiPs.png

this results a PDF
PI6zvyb.png

C#:
Document doc = new Document(Application.StartupPath + "\\customers.docx");
DataTable customersDT = General.db.GetDATATABLE("select top 5 * from customers");
customersDT.TableName = "Customers";

 //withregion signalize to earch for TableStart
doc.MailMerge.ExecuteWithRegions(customersDT);

doc.Save(string.Format(Application.StartupPath + "\\customers.pdf"));

this was an introduction because we need to go further, And the case is the join Customer & Orders ;)



using the following template

6BBWNPp.png

reference tablestart/end included on special fields
these resulted
xiETkNH.png

where inside on 3 sample pdfs, is like
YobUl2O.png

code used :
C#:
Document doc = new Document(Application.StartupPath + "\\Sonu Jain.docx");

DataTable orderTable = General.db.GetDATATABLE(@"select ShipName, orderid, OrderDate from Customers c
                                left join orders on orders.CustomerID = c.CustomerID
                                where c.CustomerID in ('ANTON','AROUT','BERGS','BLAUS')");


int counter = 0;
foreach (DataRow item in orderTable.Rows)
{
    //https://github.com/aspose-words/Aspose.Words-for-.NET/blob/6eb90e63be02ec9c157fe1a0323bb95831ca8f57/Examples/DocsExamples/DocsExamples/Mail%20Merge%20and%20Reporting/Base%20operations.cs
    //clone the document - otherwise we have to LOAD it every time
    Document dstDoc = (Document)doc.Clone(true);

    //query OrderDetails base on /item/ ORDERID field
    DataTable s = General.db.GetDATATABLE(@"select Quantity,Products.ProductName,od.UnitPrice,od.UnitPrice*Quantity as ProductPriceTotal  from (orders
                                            left join [Order Details] as od on od.OrderID = orders.OrderID)
                                            left join Products on Products.ProductID = od.ProductID
                                            where orders.OrderID =" + item["orderid"].ToString());
    s.TableName = "Orders";

    //add the /item/ aka ORDER DataRow information (ORDERID / ORDERDATE / SHIPNAME)
    dstDoc.MailMerge.Execute(item);

    //add sample field COMMENT
    //https://github.com/aspose-words/Aspose.Words-for-.NET/blob/6eb90e63be02ec9c157fe1a0323bb95831ca8f57/Examples/DocsExamples/DocsExamples/Mail%20Merge%20and%20Reporting/Working%20with%20Fields.cs#L36
    dstDoc.MailMerge.Execute(new string[] { "Comment" }, new object[] { "talamska" });

    //add the ORDER DETAILS table
    dstDoc.MailMerge.ExecuteWithRegions(s);

    //save as PDF
    counter++;
    dstDoc.Save(string.Format(Application.StartupPath + "\\order_{0}.pdf", counter++));
}

Documents merge & mail merge
C#:
//clone the document - otherwise we have to LOAD it every time
Document doc1 = (Document)General.doc1.Clone(true);
Document doc2 = (Document)General.doc2.Clone(true);
Document doc3 = (Document)General.doc3.Clone(true);
Document doc4 = (Document)General.doc4.Clone(true);

doc1.MailMerge.Execute(new string[] { "FullName" }, new object[] { "talamsca1" });
doc2.MailMerge.Execute(new string[] { "FullName" }, new object[] { "talamsca2" });
doc3.MailMerge.Execute(new string[] { "FullName" }, new object[] { "talamsca3" });
doc4.MailMerge.Execute(new string[] { "FullName" }, new object[] { "talamsca4" });

//page break between doc1 and doc2 - src - https://forum.aspose.com/t/36247
//doc2.FirstSection.PageSetup.SectionStart = Aspose.Words.SectionStart.NewPage;

doc1.AppendDocument(doc2, ImportFormatMode.KeepSourceFormatting);
doc1.AppendDocument(doc3, ImportFormatMode.KeepSourceFormatting);
doc1.AppendDocument(doc4, ImportFormatMode.KeepSourceFormatting);

doc1.Save(string.Format(AppDomain.CurrentDomain.BaseDirectory + "\\output\\{0}.pdf", customerCode));

Wingdings checkbox on mail merge

Normally you add the mailmerge fields as
qIPBd1P.png


Then on each mailmerge field you turn the font to wingdings
ebqo0uc.png


Code logic behind
C#:
// WINGDINGS symbols representation with hexadecimal value - https://www.alanwood.net/demos/wingdings.html
int certificateType = (record.certificateType.HasValue ? record.certificateType.Value : 1);
string wingdingsCheckMark = "\xf0fe";
string wingdingsEmptyCheckbox = "\xf0a8";

string wordCertType1_ADT = wingdingsEmptyCheckbox;
string wordCertType2_Passport = wingdingsEmptyCheckbox;
string wordCertType3_Other = wingdingsEmptyCheckbox;

switch (certificateType)
{
    case 1:
        wordCertType1_ADT = wingdingsCheckMark;
        break;
    case 2:
        wordCertType2_Passport = wingdingsCheckMark;
        break;
    case 3:
        wordCertType3_Other = wingdingsCheckMark;
        break;
    default:
        wordCertType1_ADT = wingdingsCheckMark;
        break;
}

//then follow the normal mailmerge procedure on those 3 fields as
//doc1.MailMerge.Execute(new string[] { "adt", "passport", "other" }, new object[] { wordCertType1_ADT, wordCertType2_Passport, wordCertType3_Other });

ref - build-table-from-datatable
 

Costas

Administrator
Staff member
you can achive the same by using the Fastreport for NET
https://www.fast-report.com/en/download/fast-report-net/

very easy and elegant...

Can export to PDF without print preview
https://stackoverflow.com/a/64475754

--

See the DataFromDataSet / ExportToPDF examples on trial package.

Add the FastReport.dll as reference to your VS Project..

No toolbox integration needed, but if you want to have it at UI, add it by right click on VS Toolbox with the option ''Choose Items' dont use drag&drop is not working....
No other DLL needs to be referenced (will carry the needed alone)... The designer exists to FastReport.Editor.dll.

Any datasource you would like to be appear on report you have to register it on report as :

C#:
    private void btnCreateNew_Click(object sender, EventArgs e)
    {
      // create report instance
      Report report = new Report();

      // register the dataset
      report.RegisterData(FDataSet);
      report.RegisterData(dT,"test");

      // enable the "Employees" datasource programmatically.
      // You can also do this in the "Report|Choose Report Data..." menu.
      report.GetDataSource("Employees").Enabled = true;

      // design the report
      report.Design();
    }

Then on the FastReport designer, Data > Choose report data..
FxqYAHz.png





In detail :

having this call :

C#:
private void ShowReportDesigner()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("code", typeof(string));
    dt.Columns.Add("name", typeof(string));
    dt.Columns.Add("quantity", typeof(int));
    dt.Columns.Add("unit_price", typeof(decimal));
    dt.Columns.Add("discount", typeof(decimal));
    dt.Columns.Add("tax", typeof(int));
    dt.Columns.Add("price_b4_tax", typeof(decimal));
    dt.Columns.Add("final_price", typeof(decimal));

    // create report instance
    Report report = new Report();

    // register the datatable
    report.RegisterData(dt, "offer");

    // design the report
    report.Design();

    // free resources used by report
    report.Dispose();
}

when press the button, getting the welcome dialog, choose blank report

Zfiw8wz.jpg

go to Data > Chooser Report Data
PnsrLPf.png

and here it is :)
KRGa27Z.png


ey3OHUq.png

adding some elements for static data (not by the datasource) and save the offer.frx near the executable

bzBVqua.png

once we try to preview it, by clicking the play button, get a bunch of errors, because the expressions are unknown for the report

jZ21ksu.png

to fix that, we have, either to define&passing them as parameters

C#:
report.RegisterData(dt, "offer");

//pass static parementers
report.SetParameterValue("invoice_type", "Προσφορά");
report.SetParameterValue("invoice_date", txtDate.Text);
report.SetParameterValue("company_name", txtName.Text);
report.SetParameterValue("afm", txtAFM.Text);
report.SetParameterValue("occupation", txtOccupation.Text);
report.SetParameterValue("doy", txtDoy.Text);
report.SetParameterValue("pay_way", txtPayWay.Text);
report.SetParameterValue("city", txtCity.Text);
report.SetParameterValue("address", txtAddress.Text);
report.SetParameterValue("deliver_way", "");

either use RegisterData and pass an IEnumerable (even you use only the 0 element) as Datasource to the report.

Further more, I added the datatable fields to Data area

szXqyvC.png

and I would liked to make the last two fields to be calculated on report (not served by datatable)... Actually those fields are
1) product price before taxes
prod_wo_tax = (unit_price - discount) * qunantity;​
2) final price
final_price = (prod_wo_tax * (tax / 100)) + prod_wo_tax;​

Always we use the format property of 'text object'. Never by script code as much possible.

At script section we declaring a private variable at class and on BeforePrint event (dont forget to tie the event from UI) we assign the shared calculation

C#:
  public class ReportScript
  {
    private decimal prod_wo_tax;
    private void Data1_BeforePrint(object sender, EventArgs e)
    {
       prod_wo_tax = ((Decimal)Report.GetColumnValue("offer.unit_price") - (Decimal)Report.GetColumnValue("offer.discount")) * ((Int32)Report.GetColumnValue("offer.quantity"));
    }
  }

then on design section, on first field we use it, as is

6T2g9UX.png

and on second field, we write this expression ;)

SQPqM9R.png

JavaScript:
[ToDecimal((prod_wo_tax*((ToInt32(Report.GetColumnValue("offer.tax")))/ ToDecimal(100)))+prod_wo_tax)]

use of FastReport, format functionality

UpXyJjj.png

A disadvantage when creating columns that is not coming from a Datasource, is that are not supporting Totals (column summation). On the other hand for columns coming by Datasource, is easy as :

OuzrnUO.png

we added the quantity field for Totals to report (yellow rectangle)

5WRs4Ii.png

on orange rectangle we have created by hand, text objects, and here is it how we do it on script section (dont forget to tie the event from UI) :

C#:
public class ReportScript
{
    private decimal prod_wo_tax;

    private decimal b4tax_total= 0;
    private decimal final_total;
    private decimal discount_total = 0;
    private decimal tax_total = 0;

    private void Data1_BeforePrint(object sender, EventArgs e)
    {
       //fields drag&drop by Datasource treeview
       prod_wo_tax = ((Decimal)Report.GetColumnValue("offer.unit_price") - (Decimal)Report.GetColumnValue("offer.discount")) * ((Int32)Report.GetColumnValue("offer.quantity"));
    }

    private void Data1_AfterPrint(object sender, EventArgs e)
    {
        //we referring direct the text_object, because is 'calculated field' as is not coming by DATASOURCE. Use of 'value' property and not to 'text' (text property contains also the currency symbol)
        b4tax_total += decimal.Parse(b4Tax.Value.ToString());
        final_total += decimal.Parse(finalPrice.Value.ToString());
 
        //use of GetColumnValue to DATASOURCE field
        discount_total += (((Decimal)Report.GetColumnValue("offer.discount")) * ((Int32)Report.GetColumnValue("offer.quantity")));
        tax_total = final_total - b4tax_total;
    }
}

also on those text objects, we use again the FastReport format functionality (to say they are currency).



Next, we define the Report Title & Page Header to be printed only on first & single page and the Page Footer only on last & single page.

9jYcIsN.png

once apply these changes all working as excepted, except that, the Page Footer is not displayed. At the designer go to Report > Options and enable the double pass.

2FgkYH5.png


Note : When there are multipage and using script variables to be displayed as 'custom' totals on PageFooter, summation calculated fine. When using the built-in 'Totals', a property needed to be adjusted.

Lastly, if we using the built-in Totals, we realize that Totals are not correct, are calculating only the last page. Be sure for each Total the option 'Reset after Print' is unchecked.

89K12Ls.png



NET documentation
https://www.fast-report.com/public_download/docs/FRNet/online/en/index.html

frx samples
https://github.com/FastReports/FastReport/tree/master/Demos/Reports

russia (most probably, previous official site)
https://xn--90aia9aifhdb2cxbdg.xn--...l/doc/designer/ReportObjectsFundamentals.html

forum
https://forum.fast-report.com/en/categories/fastreport-net

github
https://fastreports.github.io/FastReport.Documentation/Introduction.html

Report and Engine objects
https://www.fast-report.com/documentation/UserManFrNET-en/index.html?scriptreportandengine.htm
GetColumnValue(string complexName) - Returns the value of the data column. The name MUST be presented in the "DataSource.Column" form.
 
Top