Monday, 9 March 2009

Nested Mail Merge Regions using Aspose.Words

I've been looking into various solutions for merging Word documents server-side, without resorting to Microsoft Word Automation as it is apparently the devil.

After trialing a handful of solutions on the web, I've concluded that Aspose.Words has some killer features, such as doc and docx support, and PDF file export. Furthermore, unlike competing products, I won't have to sell a kidney to pay for it. Perfect!

That said, once getting into the nuts and bolts of things I discovered one shortfall that is thoroughly discussed in their forums: No support for nested mail merge regions. A region is something Aspose created to allow a portion of a Word document to grow dynamically based on a set of data, e.g. a Table.

Compared to what Microsoft Word can do, a region is pretty powerful stuff. However, not powerful enough for what I need as our app has lots of hierarchical data. For example, imagine a food order that has multiple delivery times:

10.30am
Bagels
Fruit Juice

12.30pm
Sandwiches
Tea & Coffee

If I wanted to define this in a Word template, my regions would look like this:

{TableStart:DeliveryTimes}
{TableStart:Items}
{TableEnd:Items}
{TableEnd:DeliveryTimes}

Now, for anyone who's tried, Aspose.Words will simply throw an exception: Nested mail merge regions are not yet supported.

I figured that since Aspose.Words provides a full Document Object Model for a document, I could write my own class. A bit of looping here and there... easy!

Hardly. I can now see why Aspose has been holding out on such a function. It actually turned out to be a right nightmare to write the class. That said, I got there in the end so I thought I'd share it with the rest of you so that you too don't need to toil over this for days on end.

Introducing AdvancedMailMerge

You may need to add a reference to your copy of Aspose.Words in the WordsExtensions project in order for it to compile.

In terms of how to set up your Word document regions, I've used the same syntax as Aspose.Words. Create merge fields called TableStart:TableName and TableEnd:TableName.

To perform the merge, it's pretty straightforward:

  1. Load your Word file into an Aspose.Words Document object.
  2. Create an instance of the AdvancedMailMerge object.
  3. Pass the Document into the Load method of AdvancedMailMerge. The Load method will validate the regions in the document.
  4. Check the Errors property to ensure there are no structural errors in the document.
  5. Call AddRegionData to add as many data sources as you require. There are special overloads of this function for defining child/parent relationships so that your data is filtered as per your data relationships.
  6. Call the Merge function. This will do it's magic and merge the nested regions with your data sources.
  7. You can then call the Save method of the Document object, outputting a PDF or whatever you like.

User data filtering

There is also a groovy filtering feature that allows users to filter region data by adding syntax to a TableStart merge field. If you add the \b switch to a field, followed by a standard ADO select query, the data will be filtered in the document, e.g.:

{MERGEFIELD TableStart:Items \b Title = 'Bagel' }

Switch support

Since I can't use the standard Aspose.Words MailMerge class, I'm left to support Mail Merge switches myself (e.g. \* Upper to make the result uppercase etc). I've created a MergeFieldSwitch class to handle this. However there are too many switches for me to support. I've added the basics, however they haven't really been tested. Be prepared for bugs if you want to use these.

Please Contribute

I have a lot of testing still to do on this, and I know there will be some issues. If you find any problems, please post them on the CodePlex site and I will look into it.

I'll post my own bug fixes to CodePlex as I come across them.

Cheers,
Anthony.

11 comments:

Pavithra V S said...

How do you create an AdvancedmailMerge object in Java?Does the class you developed work with Aspose.Words for Java?

Anthony said...

Sorry I only built a .NET version of the class. There may be a way to call a .net DLL from Java? Otherwise all the source code is there so you could port the class to Java...

Anonymous said...

Hello Anthony,

Are you planning to add a "MergeField" event in your code? Thank you very much for your efforts :)

Anthony said...

I've added this to the wish list, looks pretty straightforward. I'll try and take a look in the next week or so. Anthony.

Anthony said...

I've added the MergeField event. Anthony.

Anonymous said...

Thank you very much Anthony, your efforts are really amazing.

Unknown said...

Hi,

Does it support the Image:xxx
tag for rendering images in the mail merge?

Jason

Anthony said...

Yes, via the MergeImageFieldEventHandler. The usage is identical to the standard Aspose.Words Image:xxx merge field. Anthony.

Gonzalo Méndez said...

Hi Anthony,

Great work. Taking your example of the food order with it's items, is it possible to print several orders each one with their items? How do I link the order with it's children dinamically?

Anthony said...

Hi there,
Before I answer this, did you know that the latest version of Aspose.Words now supports nested regions? I haven't used it yet, but you may prefer this to using my project.

There are a few ways to achieve this. One way would be to create a top-level region which you can pass a DataTable of orders to, within this region you can output the data from the order, then place a nested region inside that has your order items.

Another way would be to generate each order separately, then append them together at the end - there is a function in Aspose.Words to do this.

Anthony.

Gonzalo Méndez said...

Hi Anthony,

Thank you very much. I didn't knew that. I' will give it a try.
Best wishes,

Gonzalo