Microsoft Dynamics AX 2009 : Creating Word documents from templates

Even though Microsoft Office Word does not have standard helper classes in Dynamics AX like Excel does, Word documents can be created using very similar procedures by calling COM objects directly. A good example of how to create Word (also PowerPoint and Visio) documents from scratch is the Task Records tool, which is a part of the Dynamics AX 2009 application (for older versions of Dynamics AX, it can be installed separately). The tool can be started from the Tools menu, and it can record a list of user actions to a Word document. The main document creation code is located in the SysTaskRecorderDocStandardWord class, and it is worth looking at the class to understand how Word documents can be created.

Another good place to start with Word documents in Dynamics AX is the standard Document handling feature. It allows building documents like a letter or fax from attached templates for any record in the system. But the usage of standard Document handling is somewhat limited, as, for example, it does not support table display methods and data with complex relations might not be displayed correctly. In such cases, customization is required.

To learn the principle in this recipe, we will add a function to the Contact Details form, which allows creating a personalized letter for a selected contact. Instead of creating a Word document from scratch, we will use a prepared template very similar way the Document handling does.

Getting ready

Before we start with the code, we create a new Microsoft Word template, and save it as Letter.dotx. Add some text and four bookmarks (Insert | Links | Bookmark menu) as per the list below:

  • ContactPerson_Name one space after Dear.

  • DirPartyTable_Name next line after Kind Regards,.

  • CompanyInfo_Name next line after DirPartyTable_Name.

  • CompanyInfo_Phone one space after Tel.:.

The letter should look like the following:

How to do it…

  1. 1. In AOT, create a new class called CreateWordLetter with the following code:

    class CreateWordLetter extends RunBase
    
    {
    DialogField dlgTemplate;
    Filename template;
    Common common;
    COM word;
    COM document;
    COM bookmarks;
    #define.Word('Word.Application')
    #define.CurrentVersion(1)
    #localmacro.CurrentList
    template
    #endmacro
    }
    public Common parmCommon(Common _common = common)
    
    {;
    common = _common;
    return common;
    }
    public static CreateWordLetter construct(Common _common)
    
    {
    CreateWordLetter createWordLetter;
    Word document, creating from templateWord document, creating from templatesteps;
    createWordLetter = new CreateWordLetter();
    createWordLetter.parmCommon(_common);
    return createWordLetter;
    }
    public container pack()
    
    {
    return [#CurrentVersion, #CurrentList];
    }
    public boolean unpack(container packedClass)
    
    {
    int version = RunBase::getVersion(packedClass);
    ;
    switch (version)
    {
    case #CurrentVersion:
    [version, #CurrentList] = packedClass;
    return true;
    default :
    return false;
    }
    return false;
    }
    protected Object dialog()
    
    {
    Dialog dialog;
    ;
    dialog = super();
    dialog.caption("Select letter template");
    dlgTemplate = dialog.addFieldValue(
    typeid(FilenameOpen), template, "Template");
    return dialog;
    Word document, creating from templateWord document, creating from templatesteps}
    public boolean getFromDialog()
    
    {;
    template = dlgTemplate.value();
    return true;
    }
    void openWord()
    
    {
    COM documents;
    ;
    try
    {
    word = new COM(#Word);
    }
    catch (Exception::Internal)
    {
    if (word == null)
    {
    throw error("Microsoft Word is not installed.");
    }
    }
    documents = word.documents();
    document = documents.add(template);
    }
    void processBookmark(str _name, anytype _value)
    
    {
    COM bookmark;
    COM range;
    ;
    if (!bookmarks.exists(_name))
    {
    return;
    }
    bookmark = bookmarks.item(_name);
    range = bookmark.range();
    range.insertAfter(_value);
    Word document, creating from templateWord document, creating from templatesteps}
    void processTable(Common _common)
    
    {
    DictTable dictTable;
    FieldId fieldId;
    DictField dictField;
    ;
    dictTable = new DictTable(_common.TableId);
    for (fieldId = dictTable.fieldNext(0);
    fieldId;
    fieldId = dictTable.fieldNext(fieldId))
    {
    dictField = dictTable.fieldObject(fieldId);
    this.processBookmark(
    dictTable.name()+'_'+dictField.name(),
    _common.(dictField.id()));
    }
    }
    public boolean validate()
    
    {;
    if (!common)
    {
    return false;
    }
    return true;
    }
    public void run()
    
    {
    EmplTable emplTable;
    ;
    this.openWord();
    bookmarks = document.bookmarks();
    emplTable = EmplTable::find(
    SysCompanyUserInfo::current().EmplId);
    this.processBookmark('Date', systemdateget());
    this.processTable(common);
    this.processTable(CompanyInfo::find());
    this.processTable(emplTable.dirPartyTable());
    word.visible(true);
    Word document, creating from templateWord document, creating from templatesteps}
    public static void main(Args _args)
    
    {
    CreateWordLetter createWordLetter;
    ;
    if (!_args || !_args.record())
    {
    throw error(Error::missingRecord(funcname()));
    }
    createWordLetter = CreateWordLetter::construct(
    _args.record());
    if (createWordLetter.prompt())
    {
    createWordLetter.run();
    }
    }
    
    
    					  
  2. 2. In AOT, create a new Action menu item with the following properties:

    PropertyValue
    NameCreateWordLetter
    ObjectTypeClass
    ObjectCreateWordLetter
    LabelCreate letter


  1. 3. Add the created menu item to the bottom of the smmContactPerson form’s ButtonGroup by creating a new MenuItemButton control with the following properties:

    PropertyValue
    NameCreateWordLetter
    MenuItemTypeAction
    MenuItemNameCreateWordLetter
    DataSourceContactPerson


  1. 4. In AOT, the form should look like the following screenshot:

  1. 5. Open Basic | Setup | Addresses | Contact Details or CRM | Contact Details and select any record:

  1. 6. Click the Create letter button, and choose the previously created Word template in the following dialog:

  1. 7. Click OK to view prepared letter. Note the data taken from the contact record, company information table, and details of the currently logged user:

How it works…

For the purpose of this recipe we created a new class, which extends the RunBase framework. This allows us to utilize its automatic features like last value or displaying dialog in our custom class. The following member methods were added:

  • parmCommon() sets or gets the common parameter.

  • construct() creates a new instance of this class.

  • pack() prepares user data to be stored for the next time.

  • unpack() retrieves stored data (if any).

  • dialog() changes the default dialog caption and adds a new file selection field.

  • getFromDialog() stores the user file selection into the template variable upon dialog closure.

  • openWord() creates a new instance of Word and initializes the document object for further use. We create this object by calling add() on the document collection object documents with the template name as an argument.

  • processBookmark() searches the collection of Word bookmarks for an existing one and inserts the given value if the bookmark is found.

  • ProcessTable() accepts any record as an argument and loops through all its fields and values trying to find an existing bookmark formatted as<table>_<field> in our Word template and insert the field value. The given format will ensure that our template is processed successfully and new bookmarks could be added without changing the Dynamics AX code.

  • validate() checks if the CreateWordLetter object is properly created, i.e. contains a value to be processed in the common variable. In our case, this is ContactPerson table records, but in theory could be any other table.

  • run() is where the major code is located. In here, we first initialize the Word application and bookmarks. The next step is to find the EmplTable table record of the currently logged user and then start inserting values into bookmarks&mdash; contact person record, company information, and employee information. And finally, we display the results.

  • main() simply puts everything together.

Once the class is ready, we create an Action menu item pointing to this class and add it to the Contact Details form as a button. This allows us to use the button to create and display a personalized letter for the selected contact person.