theThought's thoughts

Kevin A Gray - Creative Strategy Guy

Data Collection to OpenXML a simple class for populating custom parts (Technical Design)

So far in this project a survey (the survey) has been created that accepts details of a train complaint.  A Word document (the word doc) has been created that provides a printed copy of the complaint details and a custom part (the custom part) has been defined and added to the Word document to facilitate the transfer of information from IBM SPSS Data Collection to Microsoft Word.  In the last post (creating XML in Data Collection) logic was added to the end of the survey that creates an XML document containing all the responses to the survey in the same structure as the custom part in the Word document.  In this post VB will be used to create a simple class that will accept the XML document and re-build the custom part.  It will also include methods to convert the resulting document into a PDF ready for emailing to the respondent.

Technical Design

The purpose of this DLL is to handle the interaction  between Data Collection and an OpenXML document.  The class will receive the XML to be inserted into the document and the details of the custom part to be updated.  IT will then replace the content of the existing custom part with the new XML content.  It will not delete the custom part itself as this would result in the loss of the existing unique ID for the document. This would in turn cause all the document relationships and form field bindings to break.

The second part of the process is to create a PDF version of the document.  Word can now do this without the use of third party components.  The returned PDF can then be attached to an e-Mail for distribution to the survey respondent (complainant)

Update Part method
Inputs:

·         XML (containing the survey responses)

·         Document name (the name of the Word document to change)

·         Custom Part name (name of the custom part to be changed)

Outputs

·         Success/Fail Signal (indicating whether update succeeded)

Create PDF Method

Inputs: None

Outputs:

·         PDF version of document

Internal Functions

As well as methods exposed to other applications (in this case IBM SPSS Data Collection) this dll will require some internal functions.

OpenWordDoc (Opens the document defined by the Update Part Method)

Locate Custom Part (takes the open document and finds the custom part)

Adjust Custom Part (having found the custom part the XML needs to be fed in)

There is no need to save the document as the OpenXML SDK will automatically save when the custom part is changed.

Filed under  //   Custom Part   Data Collection   IBM   IBM SPSS Data Collection   OpenXML   SPSS   VB.net   XML  

Updating a Custom Part from PASW Data Collection when using OpenXML for Word (WordML)

Interlude
During the writing of this post the formal announcement regarding the acquisition of SPSS by IBM was made (IBM acquires SPSS Inc).  I now, officially work for SPSS (UK Limited), an IBM Company.  The product I run is no long PASW Data Collection but IBM SPSS Data Collection.  As a result there will be no more references to PASW.
End if Interlude

In previous posts I have explored the components required to publish a Word Document that contains a form that can be populated from IBM SPSS Data Collection.  The Word document was created using Form Fields within a table.  The Word 2007 Content Control toolkit (available here) was used to create a custom part, a slice of custom XML defined not by Word but by the developer.  The same tool was then used to link the custom parts to the form fields in the table (Binding custom parts to form fields).  All this work has resulted in a Word document that is using custom XML to define the content of the table.

The purpose of this post is to go back to IBM SPSS Data Collection to build the XML ready to be sent to Word for posting into the Word document.  There are two ways in which this can be done:

Option 1
As the custom part already exists and contains the correct structure for the XML updating the document should be just a case of updating the attributes contained within the XML.  To ensure that the custom part contains the correct information all existing attributes would have to be updated.

Option 2
Create a completely new custom part that replaces the existing custom part but keeps the same ID. 

It may be that these two techniques will blend together.  The current custom part will be read through a stream, updated and then written back.  There are only two key questions to be answered:

  • Whether it necessary to take any notice of the content that has been read into the stream?
  • Whether PASW Data Collection should assemble the new XML as a single data construct and then pass it to the dll that will post the information into the custom Part?

In the latter case the alternative is for IBM SPSS Data Collection to converse with the dll sending attribute/value pairs each time it wants to update an element of the document.  As the latter is likely to result in multiple reads of the Word document it is not considered suitable for this particular scenario.  If the document was being collaborated on such that two or more users may want to see the changes taking place in real-time it is likely that this approach would be used.

Consequently the plan at this time is to have the routing of the survey create an XML structure from the responses provided and to send that XML data structure to Word via a DLL that handles the updating of the custom part.

Creating the XML to send to the custom part
IBM SPSS Data Collection writes scripts in a VBscript like language.  It is very extendible being able to access the capabilities of any external com components available to it.  The syntax used, consequently, to build a new XML file is very recognisable to those who have written VBscript or DCScript (Data Collection Script) before:

set xmlPart = CreateObject("Microsoft.XMLDOM")

Data Collection Script (DCScript) does not used strongly typed variables but the variables do have to be declared.  Consequently a Dim statement for xmlPart is required before this script.  Once this is done it is possible to create the relevant nodes.  The first is the root node that appears at the top of the XML.  This would usually be done using createElement method, however, this method does not support namespaces so the more complex createNode is required:

set xmlRoot = xmlPart.createNode(Node_Element, "cao2:root", "http://www.kevinagray.co.uk/railcomplaint")
xmlPart.appendChild(xmlRoot)

The createNode method has a constant Node_Element that has a value of 1.  This constant is declared at the top of the script.  The createNode is followed by an appendChild method to add this node to the XML document.  This process can be repeated for the personal and incident nodes.  In the case of the incident node there is two attributes associated to this node.  The code used to generated these two attributes is the same as the code used to generate almost all the attributes in the XML document.  In each case an IF statement is used to check whether there was a response to the question.  If a response is present the value is applied to the Incident Node using the setAttribute method, otherwise the setAttribute method is used to create a blank value.  This ensures that the attribute is present so that the Word document can be mapped correctly and the structure of the XML is consistent.

if IncidentDate.Response.Value <> null then
                xmlIncident.setAttribute("cao2:when", IncidentDate.Response.Value)
else
                xmlIncident.setAttribute("cao2:when", "")
end if

if IncidentDestination.Response.Value <> null then
                xmlIncident.setAttribute("cao2:destination", IncidentDestination.Response.Value)
else
                xmlIncident.setAttribute("cao2:destination", "")
end if

The exception to this methodology is the Title and Severity questions.  Both of these questions are categorical (single punch) and so the response.value is an array (e.g. {Mr}).  Even though, being single punch these responses only ever have one element in the array it is necessary to turn the response into a text string before applying it to the XML.  Data Collection provides the format function to convert (amongst other things) categorical responses into either their categorical name or their categorical label.

Format(questionname.response.value, “a”) provides the name of the category

Format(questionname.response.value,”b”) provides the label of the category

Although as a rule DCScript is not case sensitive.  This function is one of the exceptions.  It is essential that the code (“a” or “b”) is in lower case to deliver the right result.  As a consequence the writing of these responses into the XML looks like this:

if Title.Response.Value <> null then
                xmlNode.setAttribute("cao2:title", format(Title.Response.Value,"b"))
else
                xmlNode.setAttribute("cao2:title", "")
end if

The whole routing is as follows:

Routing(Web)

Dim xmlPart, xmlNode, xmlRoot, xmlPersonal, xmlIncident
Const Node_Element = 1

                IOM.LayoutTempla te = "RailComplaint.htm"

                Personal.Ask()

                Incident.Ask()

                set xmlPart = CreateObject("Microsoft.XMLDOM")
                set xmlRoot = xmlPart.createNode(Node_Element, "cao2:root", "http://www.kevinagray.co.uk/railcomplaint")
                xmlPart.appendChild(xmlRoot)

                set xmlPersonal = xmlPart.CreateNode(Node_Element, "cao2:personal", "http://www.kevinagray.co.uk/railcomplaint")
                xmlRoot.appendChild(xmlPersonal)

                set xmlIncident = xmlPart.CreateNode(Node_Element, "cao2:incident", "http://www.kevinagray.co.uk/railcomplaint")
                xmlRoot.appendChild(xmlIncident)

                if IncidentDate.Response.Value <> null then
                                xmlIncident.setAttribute("cao2:when", IncidentDate.Response.Value)
                else
                                xmlIncident.setAttribute("cao2:when", "")
                end if

                if IncidentDestination.Response.Value <> null then
                                xmlIncident.setAttribute("cao2:destination", IncidentDestination.Response.Value)
               
else
                                xmlIncident.setAttribute("cao2:destination", "")
                end if

                set xmlNode = xmlPart.CreateNode(Node_Element, "cao2:name", "http://www.kevinagray.co.uk/railcomplaint")
                xmlPersonal.appendChild(xmlNode)

                if Title.Response.Value <> null then
                                xmlNode.setAttribute("cao2:title", format(Title.Response.Value,"b"))
                else
                                xmlNode.setAttribute("cao2:title", "")
                end if

                if fname.Response.Value <> null then
                                xmlNode.setAttribute("cao2:fname", fname.response.Value)
                else
                                xmlNode.setAttribute("cao2:fname", "")
                end if

                if lname.Response.Value <> null then
                                xmlNode.setAttribute("cao2:lname", lname.response.Value)
                else
                                xmlNode.setAttribute("cao2:lname", "")
                end if

                set xmlNode = xmlPart.CreateNode(Node_Element, "cao2:address", "http://www.kevinagray.co.uk/railcomplaint")
                xmlPersonal.appendChild(xmlNode)

                if address1.Response.Value <> null then
                                xmlNode.setAttribute("cao2:line1", address1.Response.Value)
                else
                                xmlNode.setAttribute("cao2:line1", "")
                end if

                if address2.Response.Value <> null then
                                xmlNode.setAttribute("cao2:line2", address2.Response.Value)
                else
                                xmlNode.setAttribute("cao2:line2", "")
                end if

                if Town.Response.Value <> null then
                                xmlNode.setAttribute("cao2:town", town.Response.Value)
                else
                                xmlNode.setAttribute("cao2:town", "")
                end if

                if postcode.Response.Value <> null then
                                xmlNode.setAttribute("cao2:postcode", postcode.Response.Value)
                else
                                xmlNode.setAttribute("cao2:postcode", "")
                end if

                set xmlNode = xmlPart.CreateNode(Node_Element, "cao2:contact", "http://www.kevinagray.co.uk/railcomplaint")
                xmlPersonal.appendChild(xmlNode)

                if Phone.Response.Value <> null then
                                xmlNode.setAttribute("cao2:work", Phone.Response.Value)
                else
                                xmlNode.setAttribute("cao2:work", "")
                end if

                if Phone2.Response.Value <> null then
                                xmlNode.setAttribute("cao2:home", Phone2.Response.Value)
                else
                                xmlNode.setAttribute("cao2:home", "")
                end if

                if Mobile.Response.Value <> null then
                                xmlNode.setAttribute("cao2:mobile", Mobile.Response.Value)
               
else
                                xmlNode.setAttribute("cao2:mobile", "")
                end if

                if eMail.Response.Value <> null then
                                xmlNode.setAttribute("cao2:email", eMail.Response.Value)
                else
                                xmlNode.setAttribute("cao2:email", "")
                end if

                set xmlNode = xmlPart.CreateNode(Node_Element, "cao2:departure", "http://www.kevinagray.co.uk/railcomplaint")
                xmlIncident.appendChild(xmlNode)

                if IncidentDepart.Response.Value <> null then
                                xmlNode.setAttribute("cao2:from", IncidentDepart.Response.Value)
                else
                                xmlNode.setAttribute("cao2:from", "")
                end if

                if IncidentExpected.Response.Value <> null then
                                xmlNode.setAttribute("cao2:expected", IncidentExpected.Response.Value)
                else
                                xmlNode.setAttribute("cao2:expected", "")
                end if

                if IncidentActual.Response.Value <> null then
                                xmlNode.setAttribute("cao2:actual", IncidentActual.Response.Value)
                else
                                xmlNode.setAttribute("cao2:actual", "")
                end if

                set xmlNode = xmlPart.CreateNode(Node_Element, "cao2:impact", "http://www.kevinagray.co.uk/railcomplaint")
                xmlIncident.appendChild(xmlNode)

                if IncidentSeverity.Response.Value <> null then
                                xmlNode.setAttribute("cao2:severity", format(IncidentSeverity.Response.Value, "b"))
                else
                                xmlNode.setAttribute("cao2:severity", "")
                end if

                if ExpectedOutcome.Response.Value <> null then
                                xmlNode.setAttribute("cao2:outcome", ExpectedOutcome.Response.Value)
                else
                                xmlNode.setAttribute("cao2:outcome", "")
                end if

                if CostIncurred.Response.Value <> null then
                                xmlNode.setAttribute("cao2:cost", CostIncurred.Response.Value)
                else
                                xmlNode.setAttribute("cao2:cost", "")
                end if

                set xmlNode = xmlPart.CreateNode(Node_Element, "cao2:description", "http://www.kevinagray.co.uk/railcomplaint")
                xmlIncident.appendChild(xmlNode)

                if IncidentSeverity.Response.Value <> null then
                                xmlPart.createTextNode(IncidentDescription.Response.Value)
                else
                                xmlPart.createTextNode("")
                end if

                debug.Log(xmlPart.xml)

End Routing

 

Filed under  //   DCScript   IBM   IBM SPSS Data Collection   OpenXML   PASW Data Collection   SPSS   VBScript   XML  

Data Collection Populates form using custom controls (binding custom parts to form fields)

Having created the Survey, the Form and defined the custom part it is now time to create a working prototype.  This involves creating a custom part within the document and then linking the relevant data from the custom part to each of the forms.  Word does not provide a mechanism for doing this within its interface, however there is a tool available that does do this work for you even though it’s interface is relatively crude.  This tool is called the Word 2007 Content Control Toolkit and is available via the following link (Word 2007 Content Control Toolkit).  Although this is a relatively simple interface it does allow for the creation of custom parts and the binding of form fields to those parts.  It also provides all the source code for the application so it is possible to identify how the custom part was constructed and how binding is defined.

Building the Custom Part

Before using the Word 2007 Content Control Toolkit it is advisable to create an XML document containing a static version of the XML that is to be used in the document.  The reason for this is that although this tool does provide users with the ability to build XML it does not have all the full validation and UI capabilities of a true XML creation document.  My preference is to use Microsoft’s Web Developer Express 2008.  This software is available, free of charge from Microsoft (Microsoft Express Products).  Using this tool I created a static version of the XML defined in a previous post (the custom part) and saved the file to a local directory.

Binding the Custom Part to the Fields
Once an xml document has been created the toolkit can be used to load its structure into the Complaint Record document created in Word.  Once the toolkit has been installed and opened it is necessary to point it at the Word document that contains the form layout and will contain the custom part.  This is done by simply opening the File using File Open.  Once opened the toolkit examines the document for any form fields and lists them In the window on the left (as shown in the following diagram).

Image001

The second step is create a new Custom Part.  This is done by clicking on the Create a new Custom XML Part in the bottom right corner of the dialog. Once done it is possible to load the previously created XML document (this means that the elements and attributes do not have to be created manually).  This is done by clicking on the Open XML document button.  As a result of these actions your dialog should look similar to the following:

Image002

The last step is to bind the form fields listed on the left side with the XML content on the right side.  This is done by switching the left side from Edit view to Bind view.  Bind view provides a hierarchical representation of the XML loaded into the custom part clear depicting elements and attributes.  To perform a binding perform the following steps.

Click on an element or attribute from the binding view

Click and Drag the element to the relevant field on the left side

It is important that the click and the click and drag are two separate actions, if they are done as one the wrong element/attribute from the XML is assigned to the field.  When the item is dropped onto the form field the XPath of that element is updated.  Check this to make sure it is correct.  Once all the bindings have been created the dialog should look similar to the one below:

Image003

Once all the changes have been made it is possible to save the Word Document.  Once this has been done it is possible to open the file in Word.  If the XML part loaded has sample information within it then the document will show the fields populated.  In this case it should look similar to the image below:

Image004