Ever had an InfoPath Form that you wanted to wrap up into a re-deployable feature ?
The problem is that any data connections will still point to the their original locations and the Form itself will have an incorrect PublishURL.
An InfoPath Form is really a cabinet (.cab) file. Unfortunately it's not part of the Open Office XML SDK, so you can't use the System.IO.Packaging class in the WindowsBase dll to extract it, this only works with zip files. I didn't want to sacrifice my principles and start using unmanaged code or shelling out calls to cabarc.exe, so I scoured the internet for an alternative solution. I found that the
WiX open source installer project has some assemblies that can extract and package cabinet files:
Microsoft.Deployment.Compression.Cab.dll
Microsoft.Deployment.Compression.dll
Just download and install WiX and reference the above 2 files from the WiX SDK folder.
InfoPath has the ability to use external data connection files and Sharepoint has a Data Connections Library template to store them in. This solution depends on this feature as we'll need to update any data connections that the form uses in our feature activated event reciever. I use Sharepoint 2010 Foundation which runs on my Windows 7 laptop, the Data Connections Library template isn't available out of the box in the Foundation edition, but it is part of Search Server 2010 Express. So I downloaded and installed Search Server 2010 Express, but I didn't configure it as I'm not really interested in actually using it.
As a scenario to ascertain the feasibility of creating a re-deployable InfoPath feature I created a Form that had a 2 data connection files. One was a receive data connection that was connected to the site user list and the other a submit connection to a Form Library.
Now that we have all the pre-requisites needed it's time to fire up Visual Studio and create a new empty Sharepoint 2010 project.
Add a new List Instance, I called mine Data Connections. Here is the Elements.xml file, note the TemplateType is set to 130 which is a Data Connection Library.
I added a new module called Connection Files and copied in the 2 data connection files, here's what the Elements.xml file looks like.
Now that we have a Data Connections Library with some connections in it, I'll add a new Content Type item called DCTest for the InfoPath Form. Here's the Elements.xml:
I added another Module called Form Template to the project with the xsn file in it.
Finally, just before we get into coding the feature activated event receiver I added another List Instance item to the project called Form Library.
Now that we have everything it's time to write the feature activated event receiver.
The first job it to get a reference to the Web where the feature is being activated so that we know the new url for the InfoPath Form and Data Connections. I download a copy of the XSN file to the system temp folder, where I extract it using the WiX classes. The Manifest.xsf file can then be updated before packing it back into the .xsn file and uploading it back to Sharepoint. The data connection files are easier to update as they are just xml.
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
XNamespace ns;
XDocument doc;
string tempPath = Path.GetTempPath();
string contentTypeName = "DCTest";
using (SPWeb web = (SPWeb)properties.Feature.Parent)
{
SPContentType contentType = web.ContentTypes[contentTypeName];
WebClient webClient = new WebClient();
webClient.Credentials = CredentialCache.DefaultCredentials;
webClient.DownloadFile(web.Site.Url + contentType.DocumentTemplateUrl, tempPath + contentType.DocumentTemplate);
CabInfo cab = new CabInfo(tempPath + @"\" + contentType.DocumentTemplate);
Directory.CreateDirectory(tempPath + contentTypeName);
cab.Unpack(tempPath + contentTypeName);
doc = XDocument.Load(tempPath + @"\" + contentTypeName + @"\manifest.xsf");
ns = "http://schemas.microsoft.com/office/infopath/2006/solutionDefinition/extensions";
doc.Root.Attributes("publishUrl").First().Value = web.Url + "/FormLibrary/Forms/DCTest/DCTest.xsn";
foreach (XElement elem in doc.Descendants(ns + "connectoid"))
{
elem.Attribute("siteCollection").Value = web.Site.Url;
elem.Attribute("source").Value = "/forms/DataConnections/" + elem.Attribute("source").Value.Substring(elem.Attribute("source").Value.LastIndexOf(@"/") + 1);
}
doc.Save(tempPath + @"\" + contentTypeName + @"\manifest.xsf");
cab.Pack(tempPath + @"\" + contentTypeName);
webClient.Headers[HttpRequestHeader.ContentType] = "application/octet-stream";
webClient.UploadFile(web.Site.Url + @"/forms/_cts/DCTest/DCTest.xsn", "PUT", tempPath + @"\" + contentType.DocumentTemplate);
//Remove the default content type and add the site content type
SPList library = web.Lists["Form Library"];
library.ContentTypes[0].Delete();
library.ContentTypes.Add(web.ContentTypes["DCTest"]);
//Update the data connections
SPList list = web.Lists["Data Connections"];
foreach (SPFile file in list.RootFolder.Files)
{
using (StreamReader sr = new StreamReader(file.OpenBinaryStream()))
{
doc = XDocument.Parse(sr.ReadToEnd());
}
ns = "http://schemas.microsoft.com/office/infopath/2006/udc";
if (file.Name == "DC001-Receive.udcx")
{
doc.Descendants(ns + "WebUrl").First().Value = web.Url;
doc.Descendants(ns + "ListId").First().Value = "{" + web.SiteUserInfoList.ID.ToString() + "}";
}
else if (file.Name == "DC001-Submit.udcx")
{
doc.Descendants(ns + "FolderName").First().Value = web.Url + "/FormLibrary";
}
byte[] byteArray = Encoding.ASCII.GetBytes(doc.Root.ToString());
MemoryStream stream = new MemoryStream(byteArray);
file.SaveBinary(stream);
}
}
}