Sunday, February 1, 2009

Getting and Updating InfoPath XML in WF Workflow

Background

Some time ago I started to have to build some WF Workflows with InfoPath. In this blog posting, I discuss the life-cycle of InfoPath forms and how I would architect an InfoPath WF flow solution. In my design pattern, I have seen better implementations where the core InfoPath form is separated from the workflow association, initiation and tasks forms. I really think it is better to use the workflow forms only for managing workflow state and let the core InfoPath form be the piece of SharePoint content that is being automated.

There will come a time in your WF flow where you will want to retrieve data from your InfoPath form and set data in your InfoPath form. At first, you may say, well that is easy because in the workflow you have a SPWorkflowActivationProperties object which has a reference to the list item.

The goal here is create a utility that I can re-use in other WF solutions, so that I can quickly get access to the XML data in the InfoPath form and have the ability to modify it.

Step 1 – Create Class from XSD

To start off with, I needed to generate a class from the XSD. It is pretty well documented on how to get the main schema out of an InfoPath form .xsn file. Once I get the myschema.xsd file out of the .xsn, I run the following command line:

xsd myschema.xsd /c /n:WFDistillery.Utilities.WF /o:C:\WFDistillery\Utilities

All I basically doing is taking the xsd from the InfoPath form, creating a class with a designed namespace and outputting it to a specified location. The name of the class generated will be driven off the name of the top group node in the InfoPath form. When a new InfoPath form is created, the root group node will be called myFields. I would recommend changing that before generating the class to something like TravelRequestForm, ContractRequestForm, etc. you get the point. Then the generated class will have that name.

Step 2 – Create a FormLoader Class

Next I created an abstract class called FormLoader. The job of this class is to perform the core implementation of loading and saving the xml back into the document library. In this class:

  • The class is abstract and has three methods that must be implemented.
  • The class expects to receive the SPListItem from the workflow and will load up the XSD class from it.
  • The SaveForm method has the most implementation. Specifically, care has to be take to preserve the processing instructions that are in the InfoPath XML. The deserialization of the class from the InfoPath XML will lose the processing instructions. Notice I keep the XML string as a local variable to the side.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;

namespace WFDistillery.Utilities.WF
{
public abstract class FormLoader
{
#region Attributes

protected SPListItem _infoPathItem;
protected string _infoPathXML = "";

#endregion

#region Constructor

/// <summary>
/// This will load the form object.
/// </summary>
/// <param name="infoPathItem"></param>
public FormLoader(SPListItem infoPathItem)
{
_infoPathItem = infoPathItem;
GetFormXML();
LoadClass();
}

#endregion

#region Methods

/// <summary>
/// Get the XML from SharePoint
/// </summary>
private void GetFormXML()
{
//Get the XML
byte[] fileBytes = _infoPathItem.File.OpenBinary();
UTF8Encoding encoding = new UTF8Encoding();
String xml = encoding.GetString(fileBytes);

_infoPathXML = xml.Trim();
}

/// <summary>
/// Deserailize the XML into class
/// </summary>
protected abstract void LoadClass();

protected abstract Type GetFormType();

protected abstract object GetFormObject();

/// <summary>
/// Save the XML back to SharePoint
/// </summary>
public void SaveForm()
{
if (_infoPathItem != null)
{
//---------------------
//Serialize - UTF-8 issue
//http://ashish.tonse.com/2008/04/serializing-xml-in-net-utf-8-and-utf-16/

XmlSerializer serializer = new XmlSerializer(GetFormType());

//Create a MemoryStream here, we are just working exclusively in memory
Stream stream = new MemoryStream();

//The XmlTextWriter takes a stream and encoding as one of its constructors
XmlTextWriter writer = new XmlTextWriter(stream, Encoding.UTF8);
serializer.Serialize(writer, GetFormObject());
writer.Flush();

//Go back to the beginning of the Stream to read its contents
stream.Seek(0, SeekOrigin.Begin);

//Read back the contents of the stream and supply the encoding
StreamReader streamReader = new System.IO.StreamReader(stream, Encoding.UTF8);

string serializedXML = streamReader.ReadToEnd();

//-----------------
//Processing Instructions for InfoPath are lost, need to get them back
//Load the contents of the XML document
XmlDocument docOldXml = new XmlDocument();
docOldXml.LoadXml(_infoPathXML);

//Read processing instruction from document
XmlProcessingInstruction pi1 = (XmlProcessingInstruction)docOldXml.SelectSingleNode
("/processing-instruction(\"mso-infoPathSolution\")");
XmlProcessingInstruction pi2 = (XmlProcessingInstruction)docOldXml.SelectSingleNode
("/processing-instruction(\"mso-application\")");

//-----------------
//Rebuild the XML with instruction
XmlDocument docNewXml = new XmlDocument();

docNewXml.LoadXml(serializedXML);

XmlProcessingInstruction piNew2 = docNewXml.CreateProcessingInstruction("mso-application", pi2.Value);
docNewXml.InsertBefore(piNew2, docNewXml.ChildNodes[1]);

XmlProcessingInstruction piNew1 = docNewXml.CreateProcessingInstruction("mso-infoPathSolution", pi1.Value);
docNewXml.InsertBefore(piNew1, docNewXml.ChildNodes[1]);

//-----------------
//Save the Xml back to item
MemoryStream fileStream = new MemoryStream();
docNewXml.Save(fileStream);

_infoPathItem.File.SaveBinary(fileStream);
_infoPathItem.File.Update();

//-----------------
//Reset the XML
_infoPathXML = docNewXml.OuterXml;
}
}

#endregion
}
}

Step 3 – Create an implementation of the FormLoader

The following is a class that inherits from FormLoader. In this case, you will see I have a ContactApprovalForm that will be loaded up.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using Microsoft.SharePoint;

namespace WFDistillery.Utilities.WF
{
public class ContractApprovalFormLoader : FormLoader
{
#region Attributes

ContractApprovalForm _contractForm;

#endregion

#region Constructor

public ContractApprovalFormLoader(SPListItem infoPathItem) : base(infoPathItem)
{

}

#endregion

#region Properties

public ContractApprovalForm ContractApprovalForm
{
get
{
return _contractForm;
}
}

#endregion

#region Abstract Methods

/// <summary>
/// Deserailize the XML into class
/// </summary>
protected override void LoadClass()
{
if (!String.IsNullOrEmpty(_infoPathXML))
{
XmlSerializer serializer = new XmlSerializer(GetFormType());
XmlTextReader reader = new XmlTextReader(new StringReader(_infoPathXML));
_contractForm = (ContractApprovalForm)serializer.Deserialize(reader);
}
}

protected override Type GetFormType()
{
return typeof(ContractApprovalForm);
}

protected override object GetFormObject()
{
return (object)_contractForm;
}

#endregion
}
}

Step 4 – Using the Loader class.

Now using these classes in your workflow is a simple.

In my workflow class I added the following item to avoid the issue I wrote about in this blog posting. All I am doing is loading up the SPListItem every time I reference it.

private SPListItem WorkflowItem
{
get
{
SPDocumentLibrary library =
(SPDocumentLibrary)WorkflowProperties.Web.Lists[WorkflowProperties.ListId];

return library.GetItemById(WorkflowProperties.ItemId);
}
}

Then all I need to do is add a code event and drop in the following three lines of code. All I do is pass in the workflow SPListItem which will load up the item. On my XSD class I have a Status field which I update to open. Then I call SaveForm() and the job is done.

ContractApprovalFormLoader loader = new ContractApprovalFormLoader(WorkflowItem);
loader.ContractApprovalForm.Request.Status = "Open";
loader.SaveForm();

Final Thoughts

I could use this implementation with other things in SharePoint, like an event hander that put into a Form Library. As well, you do this from a strongly typed dataset if that is what you prefer. The implementation really would not change too much – for instance you will not have to worry about losing the processing instructions.

If only I could have K2 all the time, I would not have to deal with this mess at all…

1 comment:

Unknown said...

I'm unfortunately working on a project that requires me to use VB.Net, and lots of the auto-conversion tools were giving me errors when converting your code to VB automatically (I believe because of how VB.Net handles constructors slightly differently).

In case anyone needs this in VB.Net, here's the code

Imports System.Collections.Generic
Imports System.IO
Imports System.Text
Imports System.Xml
Imports System.Xml.Serialization
Imports System.Xml.XPath
Imports Microsoft.SharePoint
Imports Microsoft.SharePoint.Workflow


Public MustInherit Class FormLoader
Protected _infoPathItem As SPListItem
Protected _infoPathXml As String = ""

Public Sub New()

End Sub

Public Sub New(ByVal ipItem As SPListItem)
_infoPathItem = ipItem
GetFormXml()
LoadClass()
End Sub

Private Sub GetFormXml()
Dim fileBytes As Byte() = _infoPathItem.File.OpenBinary()
Dim encod As UTF8Encoding = New UTF8Encoding()
Dim xml As String = encod.GetString(fileBytes)
_infoPathXml = xml.Trim()

End Sub

Protected MustOverride Sub LoadClass()
Protected MustOverride Function GetFormType() As Type
Protected MustOverride Function GetFormObject() As Object

Public Sub SaveForm()
If Not _infoPathItem Is Nothing Then
''Serialize - UTF-8 issue
''http://ashish.tonse.com/2008/04/serializing-xml-in-net-utf-8-and-utf-16/
Dim serializer As XmlSerializer = New XmlSerializer(GetFormType())

''Create a MemoryStream here, we are just working exclusively in memory
Dim str As Stream = New MemoryStream()
''The XmlTextWriter takes a stream and encoding as one of its constructors
Dim writer As XmlTextWriter = New XmlTextWriter(str, Encoding.UTF8)
serializer.Serialize(writer, GetFormObject())
writer.Flush()
''The XmlTextWriter takes a stream and encoding as one of its constructors
str.Seek(0, SeekOrigin.Begin)
''Read back the contents of the stream and supply the encoding
Dim strReader As StreamReader = New StreamReader(str, Encoding.UTF8)
Dim serializedXml As String = strReader.ReadToEnd()
''Processing Instructions for InfoPath are lost, need to get them back
''Load the contents of the XML document
Dim docOldXml As XmlDocument = New XmlDocument()
docOldXml.LoadXml(_infoPathXml)
Dim pi1 As XmlProcessingInstruction = CType(docOldXml.SelectSingleNode("/processing-instruction(""mso-infoPathSolution"")"), XmlProcessingInstruction)
Dim pi2 As XmlProcessingInstruction = CType(docOldXml.SelectSingleNode("/processing-instruction(""mso-application"")"), XmlProcessingInstruction)

''Rebuild the XML with instruction
Dim docNewXml As XmlDocument = New XmlDocument()
docNewXml.LoadXml(serializedXml)
''Read processing instruction from document
Dim piNew2 As XmlProcessingInstruction = docNewXml.CreateProcessingInstruction("mso-infoPathSolution", pi2.Value)
docNewXml.InsertBefore(piNew2, docNewXml.ChildNodes(1))
Dim piNew1 As XmlProcessingInstruction = docNewXml.CreateProcessingInstruction("mso-application", pi1.Value)
docNewXml.InsertBefore(piNew1, docNewXml.ChildNodes(1))

''Save the XML back to item
Dim fileStream As MemoryStream = New MemoryStream()
docNewXml.Save(fileStream)
_infoPathItem.File.SaveBinary(fileStream)
_infoPathItem.File.Update()

''Reset the xml
_infoPathXml = docNewXml.OuterXml



End If
End Sub
End Class