Showing posts with label Infopath. Show all posts
Showing posts with label Infopath. Show all posts

Packaging InfoPath Forms into Site Features

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);
                }
            }
        }

Using Sharepoint UserGroup Web Service with InfoPath Forms Service

When you create a data connection to the UserGroup Sharepoint Web Service and select the GetUserCollectionFromGroup method, Infopath doesn’t interpret the results correctly and you can’t use the data connection in a Drop Down List control. I’ve seen examples which use code to get around this problem, but I’m using the Forms Service so code is not an option. We need to modify Infopath’s xsd file of the web service data connection to fix the problem.

In the example below I will create a data connection to the http://moss/_vti_bin/usergroup.asmx web service to receive data to use in a Drop Down List to pick from the members of a Sharepoint Group.

1. Create the Data Connection


2. Select Web Service


3. Enter the URL to the UserGroup web service, don’t forget to include your site in the URL if appropriate.


4. For this example I’m using the GetUserCollectionFromGroup method


5. Provide InfoPath with an example Group Name
6. Provide InfoPath with the actual Group Name you want this Data Connection to query
7. Choose if you want to store a copy of the data in the form template for use in Offline mode. Since I’m using the Forms Service the form will never be used offline, so I’m leaving the box unchecked
8. Give your new Data Connection a name and Finish the wizard


Next we need to save the Form so we can manually edit the XSD for the Data Connection. From the File drop down manu choose Save as Source Files…
Open the folder where you saved the Form and you should see a Developers1.xsd file, edit this file with your favourite XML Editor (or notepad). Update the file so it looks like the snippet below, I’ve put in a comment at the start and end of the modification.

<schema elementformdefault="qualified" targetnamespace="http://schemas.microsoft.com/sharepoint/soap/directory/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:tns="http://schemas.microsoft.com/sharepoint/soap/directory/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
 <complextype name="GetUserCollectionFromGroupType">
  <sequence>
   <element maxoccurs="1" minoccurs="0" name="Users">
    <complextype>
     <sequence>
      <element maxoccurs="unbounded" name="User">
       <complextype>
        <attribute name="Notes" type="s:string"></attribute>
        <attribute name="Name" type="s:string"></attribute>
        <attribute name="IsSiteAdmin" type="s:string"></attribute>
        <attribute name="Sid" type="s:string"></attribute>
        <attribute name="ID" type="s:string"></attribute>
        <attribute name="LoginName" type="s:string"></attribute>
        <attribute name="Email" type="s:string"></attribute>
        <attribute name="IsDomainGroup" type="s:string"></attribute>
       </complextype>
      </element>
     </sequence>
    </complextype>
   </element>
  </sequence>
 </complextype> 
 <element name="GetUserCollectionFromSite">
  <complextype></complextype>
 </element>
</schema>
We need to change another part of this file, replacing a s:element block.
<!--<s:element name="GetUserCollectionFromGroup">
  <s:complexType>
    <s:sequence>
      <s:element minOccurs="0" maxOccurs="1" name="groupName" type="s:string"></s:element>
    </s:sequence>
  </s:complexType>
</s:element>-->
<!-- Beginning of insert -->
<s:element name="GetUserCollectionFromGroup" type="tns:GetUserCollectionFromGroupType" />
<!-- End of insert -->
Now save the Developers1.xsd file, right click on the Manifest.xsf file and choose Design.

Add a Drop Down List Control and set its Data Source to Developers, you should now be able to select the correct attributes returned from the web service.

You can Save the Form as a standard XSN file, but DO NOT edit the Developers Data Source in InfoPath or you will overwrite the changes made to the Developers1.xsd file.