theThought's thoughts

Kevin A Gray - Creative Strategy Guy

IBM SPSS Data Collection Consumes DLL to push data into OpenXML

 

So the class required to push data into an OpenXML Word document has now been created (the class), there are just three steps left:

·         Create the DLL wrapper and publish

·         Create a test Chassis to ensure that the DLL works and resolve issues

·         Integrate the DLL into the Data Collection routing and test the final integration

Details of each of these steps is below:

Create the DLL wrapper and publish

Data Collection is able to integrate with processes that are exposed as COM components.  At present the DLL that has been created is not exposed in this way consequently some changes have to be made. This is done by changing the definition of the class and is described by the following article (com objects in vb.net).  Many of these steps have already taken place.  As this particular class does not use or consume events I did not create an Events Id.  Also I used Visual Basic Express 2008 to create my DLL and this does not have the GUID creating tool referenced in the document.  Consequently I used a website to do this (oh the beauty of the internet),  this site is fantastic in its simplicity (create a GUID).

Effectively I did two things:

I replaced the Class definition

Public Class Complaint

with

<System.Runtime.InteropServices.ProgId("cao2.Complaint"), ComClass(Complaint.ClassId, Complaint.InterfaceId)> Public Class Complaint

   Public Const ClassId As String = "CB3F9E3E-D1EB-4852-9C34-7C0A6DC4F7F8"
   Public Const InterfaceId As String = "9C10A919-8E4B-419E-9AAE-3868A1466EC4"

Following this two Constants are generated for the ClassID and InterfaceID GUIDs.  They are kept as constants to make sure that they are the same between compilations of the DLL

Lastly the Properties of the Project are changed so that the Assembly knows it needs to make the assemblies COM visible.  This setting is shown in the following diagram:

Image001

Create a test Chassis to ensure that the DLL works and resolve issues

Once the DLL has been constructed it needs to be tested.  A test chassis is needed to be able to define the DLL and pass the UpdateCustomPart method with the necessary details.  I used a project I had created earlier (first project) that I built as a chassis as it has a form interface and already has the right references. I added a RichTextBox so that the output from the survey (survey output) could be copied into the Chassis and passed to the DLL.  The form now like the one shown below:

Image002

Once the form was finished the references within the project were changed so that the chassis is aware of the newly created DLL.  The reference can be seen on the second line of the following diagram.  It was created by adding a reference and pointing to the DLL that was created when the DLL was compiled.

Image003

Next the start button was re-coded so that it creates a new instance of the RailComplaint class and then calls the UpdateCustomPart passing the relevant information into the method.  The information it passes is the name and location of the Word Document (hard coded for this test), the name of the CustomPart (again hard coded), and the content of the rich text box that should contain the XML string from the survey.

Private Sub pbnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles pbnStart.Click

‘Update Custom Part Test

Dim RailComplaint As New RailComplaint.Complaint
Dim Result As Boolean

Result = RailComplaint.UpdateCustomPart("C:\projects\OpenXML\Samples\RailComplaint\Customer Complaint Test.docx", "item1", rtbXML.Text)

End Sub

Once this is complete IBM SPSS Data Collection – Author Professional is run  and the survey is loaded.  Auto-Answer is used to populate the responses and generate the XML.  The last line of code sends the XML to the output window: Debug.log(xmlPart.xml).  Once the survey is complete the contents of the output window can be copied into the RichTextbox in the test Chassis.  Finally, the start button can be pressed and the code executed.  The script provided in this and other posts has already gone through this process so should work without issue.

Integrate the DLL into the Data Collection routing and test the final integration

The final stage of the process is to call the DLL from the survey.  The code used to do this is very similar to that used in the test harness.  DCScript variables are not typed so the declarations are simpler.  The last line just makes sure that the RailComplaint object is completely removed from memory.

Dim Result
Dim RailComplaint          

                set RailComplaint = CreateObject("cao2.Complaint")
                Result = RailComplaint.UpdateCustomPart("C:\projects\OpenXML\Samples\RailComplaint\Customer Complaint Test.docx", "item1", xmlPart.xml)
                set RailComplaint = null

Once this has been done it is possible to run the survey, complete the two pages of questions and then Open the Customer Complaint Text.docx file to see the result.

Filed under  //   CreateObject   DCScript   DLL   IBM   OpenXML   PASW DATA Collection   SPSS   WordML  

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