Monday, February 16, 2009

Custom SharePoint Web Service – Upload InfoPath Attachment

1.0 Creating the Utility


1.1 Introduction

This blog will hopefully help when trying to get attachments out of InfoPath forms and into SharePoint document libraries. If you are doing standard MOSS and InfoPath development, you are going to want to eventually get the attachments out of the InfoPath form. For instance when you search for attached documents, if they are still in the InfoPath form, search cannot find them because they are a binary string inside the InfoPath instance.

For K2 developers many of you know of the storage ramifications if the InfoPath form stores the attachment binary. I wrote a blog back in the K2.net 2003 days where I wrote a custom web service that would using the K2 API to save the attachment off in into SharePoint 2003. Some others came up with other solutions like using K2 SmartObjects, or extracting the binary out of the XML before the process instance starts. I just figured that there must be a better way of doing this in a generic way.

1.2 Solution

The solution is to create a custom SharePoint web service that runs under the context of SharePoint. Why did I decide to ultimately use a custom SharePoint service to implement this solution?

  • I wanted to use the WSS 3.0 SharePoint API to implement this. I wanted to make this reusable solution whether I am doing K2 development or not.
  • I wanted to use the SharePoint context to upload the document. When a document is uploaded in the fashion, the user must have permission to the document library. Now I get security for free.
  • SharePoint will upload the file under the current user account. If I had done this as an external web service, the username that would have uploaded the file would the system account that the web service is running under; instead of the actual user who uploaded the document.

I will provide a detailed solution on how to get InfoPath attachments out of the forms and into SharePoint document libraries. This solution is geared towards anyone doing MOSS and InfoPath development. The solution is straight forward:

  • First, I will create a utility class that will handle the saving of a attachment binary into SharePoint.
  • Second, I will create a custom SharePoint service to wrap these methods.
  • Third, I will show how to deploy the custom service in a WSP solution.
  • Fourth, I will show how to wire up the custom service in an InfoPath form.

2.0 Upload Utilities


Before going off and creating this web service, let's create the utilities that will upload the files.

  • I will create a utility class that will be responsible for uploading the file.
  • I will also create a different class to assist with the creation of folders for uploaded documents. The reason why I provided this is because when building a workflow for the InfoPath form, you will probably have a unique key that is for the process instance (WF, K2, etc). For attached items, you will want to store them by process instance so you do not have attachment names conflict.
  • I will also write another class that will get the files so that you can display links to all the files in the InfoPath form.

The ultimate user experience is that they will not ever know that they are uploading the document to a SharePoint library and it is completely transparent.

I will create a C# class library called WSSDistillery.Utilities. The reason why I am doing this is because all of these methods can be completely reusable in a list event handler, in a WF workflow, web part, etc. All the custom service is going to do is wrap the method calls.

In this project add references to Microsoft.SharePoint and System.Web.

2.1 Upload Attachments


The trick associated to getting an attachment out of an InfoPath form is removing the header. What happens is when an attachment is pulled into an InfoPath form; InfoPath will add a header into the binary of the attachment that captures information like the name and size of the file. We need to rip that out and only send the binary of the attachment itself into a document library. Plus we want to make sure the name of the file set into SharePoint too.

The following class will show you how to do it. Now in my old blog I show how to do it. I wanted to rewrite it plus I wanted to do it in a better way. I found this blog which made it so much simpler. I have repurposed that code here.


public class InfoPathForm
{
/// <summary>
/// Uploads an InfoPath attachment to a list and folders. Method
/// only supports saving items up to two folders deep.
/// </summary>
/// <param name="siteURL"></param>
/// <param name="webUrl"></param>
/// <param name="docLibName"></param>
/// <param name="parentFolderName">Pass NULL if there is no folder.</param>
/// <param name="subFolderName">Pass NULL if there is no sub folder.</param>
/// <param name="infopathAttachment"></param>
/// <param name="overwrite"></param>
public static void UploadAttachmentFile(string siteURL, string webUrl,
string docLibName, string parentFolderName, string subFolderName,
byte[] infopathAttachment, bool overwrite)
{
try
{
MemoryStream stream = new MemoryStream(infopathAttachment);
BinaryReader reader = new BinaryReader(stream);

//Read header
reader.ReadBytes(16); // Skip the header data.
int fileSize = (int)reader.ReadUInt32();
int attachmentNameLength = (int)reader.ReadUInt32() * 2;
byte[] fileNameBytes = reader.ReadBytes(attachmentNameLength);

//Get the attachment name
Encoding enc = Encoding.Unicode;
string attachmentName = enc.GetString(fileNameBytes, 0, attachmentNameLength - 2);

//Get the real attachment without InfoPath header
byte[] attachment = reader.ReadBytes(fileSize);

using (SPSite site = new SPSite(siteURL))
{
using (SPWeb web = site.OpenWeb(webUrl))
{
SPList list = web.Lists[docLibName];
if (String.IsNullOrEmpty(parentFolderName))
{
//Save document directly into list.
list.RootFolder.Files.Add(attachmentName, attachment, overwrite);
}
else
{
if (string.IsNullOrEmpty(subFolderName))
{
//Save document in folder in root of list.
list.RootFolder.SubFolders[parentFolderName].Files.Add(
attachmentName, attachment, overwrite);
}
else
{
//Save document in a sub folder.
list.RootFolder.SubFolders[parentFolderName].SubFolders[subFolderName].Files.Add(
attachmentName, attachment, overwrite);
}
}
}
}
}
catch (Exception ex)
{
throw new Exception("Error saving attachment >> " + ex.Message.ToString());
}
}
}

2.2 Folder Methods

The following are the folder methods that I will use to create a folder in the root of a SPList. I also have a method for creating a sub folder. I really did not try to make this handle creating folders deeper than two levels. I also know it possible to write something a little more generic however I just want to expose allowing external callers to create a parent and a sub directory only.

public class Folder
{
/// <summary>
/// Create a folder in the root of a list.
/// </summary>
/// <param name="siteURL"></param>
/// <param name="webUrl"></param>
/// <param name="docLibName"></param>
/// <param name="folderName"></param>
/// <returns></returns>
public static SPListItem CreateTopFolder(string siteURL, string webUrl,
string docLibName, string folderName)
{
SPList list = GetList(siteURL, webUrl, docLibName);

return CreateFolder(list,
folderName,
list.RootFolder.ServerRelativeUrl,
null);
}

/// <summary>
/// Creates a sub folder using a reference to a parent folder.
/// </summary>
/// <param name="siteURL"></param>
/// <param name="webUrl"></param>
/// <param name="docLibName"></param>
/// <param name="parentFolderName"></param>
/// <param name="subFolderName"></param>
/// <returns></returns>
public static SPListItem CreateSubFolder(string siteURL, string webUrl,
string docLibName, string parentFolderName, string subFolderName)
{
SPList list = GetList(siteURL, webUrl, docLibName);

return CreateFolder(list,
subFolderName,
list.RootFolder.ServerRelativeUrl + "/" + parentFolderName,
parentFolderName);
}

//Get the list
private static SPList GetList(string siteURL, string webUrl,
string docLibName)
{
using (SPSite site = new SPSite(siteURL))
{
using (SPWeb web = site.OpenWeb(webUrl))
{
return web.Lists[docLibName];
}
}
}

/// <summary>
/// Creates a folder.
/// </summary>
/// <param name="list"></param>
/// <param name="folderName"></param>
/// <param name="folderUrl"></param>
/// <param name="parentFolder"></param>
/// <returns></returns>
public static SPListItem CreateFolder(SPList list, string folderName,
string folderUrl, string parentFolder)
{
SPListItem folder = null;
foreach (SPListItem f in list.Folders)
{
if (f.Name == folderName)
{
folder = f;
break;
}
}

if (folder == null)
{
folder = list.Items.Add(folderUrl,
SPFileSystemObjectType.Folder, parentFolder);

if (folder != null)
{
folder["Name"] = folderName;
folder["Title"] = folderName;
folder.Update();
}
}

return folder;
}
}

2.3 Get Files
This method will return all of the files for a specified SPList or folder. Right now it is only geared towards going two layers deep. I could write something that is recursive where I can say get all files from a specified folder at a specific level however right now that is not needed.

public class Items
{
/// <summary>
/// Method will return all of the files in a specified folder. It is
/// limited from only getting two levels deep.
/// </summary>
/// <param name="siteURL"></param>
/// <param name="webUrl"></param>
/// <param name="docLibName"></param>
/// <param name="parentFolderName">optional</param>
/// <param name="subFolderName">optional</param>
/// <returns></returns>
public static SPListItemCollection GetItems(string siteURL, string webUrl,
string docLibName, string parentFolderName, string subFolderName)
{
try
{
SPListItemCollection items = null;

using (SPSite site = new SPSite(siteURL))
{
using (SPWeb web = site.OpenWeb(webUrl))
{
SPList list = web.Lists[docLibName];
SPFolder folder = null;

if (String.IsNullOrEmpty(parentFolderName))
{
//Get from root
folder = list.RootFolder;
}
else
{
if (string.IsNullOrEmpty(subFolderName))
{
//Get from first level of folders
folder = GetFolder(list.RootFolder.SubFolders, parentFolderName);
}
else
{
//Get from second level of folders
folder = GetFolder(list.RootFolder.SubFolders[parentFolderName].SubFolders,
subFolderName);
}
}

//Get the items
if (folder != null)
{
SPQuery query = new SPQuery();
query.Folder = folder;

items = web.Lists[docLibName].GetItems(query);
}

return items;
}
}
}
catch (Exception ex)
{
throw new Exception("Error creating list of items >> " + ex.Message.ToString());
}
}

private static SPFolder GetFolder(SPFolderCollection folders, string folderName)
{
foreach (SPFolder folder in folders)
{
if (folder.Name == folderName)
{
return folder;
}
}

return null;
}
}

3.0 Creating the Custom SharePoint Web Service

The next step is to create the custom SharePoint web service. There is a MSDN Article and SDK article but in this article I have streamlined it. Here is another blog I found but it has some issues associated to the deployment.

3.1 Create a web service project


Create a web service project called WSSDistillery.Utilities.Service.InfoPath with a web service called InfoPathAttachment.asmx.

3.2 Create Web Methods


I then added a reference to the WSSDistillery.Utilities project I created in the previous section. You will need to add reference to Microsoft.SharePoint. All my web service does is wrap and expose the methods from my utility class.

public class InfoPathAttachment : System.Web.Services.WebService
{

[WebMethod]
public void UploadInfoPathAttachment(string siteURL, string webUrl,
string docLibName, string parentFolderName, string subFolderName,
byte[] attachment, bool overwrite)
{
InfoPathForm.UploadAttachmentFile(siteURL, webUrl, docLibName,
parentFolderName, subFolderName, attachment, overwrite);
}

[WebMethod]
public void CreateFolder(string siteURL, string webUrl,
string docLibName, string folderName)
{
Folder.CreateTopFolder(siteURL, webUrl, docLibName, folderName);
}

[WebMethod]
public void CreateSubFolder(string siteURL, string webUrl,
string docLibName, string parentFolderName, string subFolderName)
{
Folder.CreateSubFolder(siteURL, webUrl, docLibName, parentFolderName, subFolderName);
}

[WebMethod]
public XmlDocument GetAttachments(string siteURL, string webUrl,
string docLibName, string parentFolderName, string subFolderName)
{
StringBuilder xml = new StringBuilder();
xml.Append("<items>");

SPListItemCollection items = WSSDistillery.Utilities.Items.GetItems(
siteURL, webUrl, docLibName, parentFolderName, subFolderName);

foreach (SPListItem item in items)
{
//Do not include folders items
if (item.Folder == null)
{
xml.AppendFormat("<item><name>{0}</name><url>{1}</url></item>",
item.Name, HttpUtility.UrlPathEncode(item.Web.Url + "/" + item.Url));
}
}

xml.Append("</items>");

//InfoPath can consume an XML Document easily
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml.ToString());
return xmlDoc;
}
}

3.2.1 Remove Code Behind Reference



After creating the service, right click InfoPathAttachment.asmx, click on View Mark Up and remove the code behind attribute leaving just.

<%@ WebService Language="C#" Class="WSSDistillery.Utilities.Service.InfoPath.InfoPathAttachment" %>

3.3 Create the Discovery and WSDL files

Next we need create the discovery and WSDL files. Now these steps are a little strange and may not make complete sense at first. Basically we have to make the service discoverable inside of SharePoint.

First what I did was create a virtual directory in IIS that pointed to my web service project. So my project is located at C: \WSSDistillery\Utilities\WSSDistillery.Utilities.Service.InfoPathAttachment and I point the virtual directory to use those files. The name of virtual directory is TestInfoPathAttachment. Then I opened the .NET command line window and ran the following command.

disco http://localhost/TestInfoPathAttachment/InfoPathAttachment.asmx

Second, this will generate two files (InfoPathAttachment.wsdl and InfoPathAttachment.disco) that are located at C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin. Go get those files, copy them into your web service project directory and add them into your Visual Studio project by adding existing items. Rename the files to be the following:

  • Change InfoPathAttachment.disco to InfoPathAttachmentdisco.aspx
  • Change InfoPathAttachment.wsdl to InfoPathAttachmentwsdl.aspx

Third, in both files do the following. Remove the XML tag line:


<?xml version="1.0" encoding="utf-8"?>

And replace it with this:

<%@ Page Language="C#" Inherits="System.Web.UI.Page" %>
<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint.Utilities" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<% Response.ContentType = "text/xml"; %>

Be really careful when doing this because Visual Studio will sometimes try to add in double quote marks. If that happens, get rid of them because InfoPath will break when making the web service call. Someone actually mentioned this in the MSDN Article and I would have been scratching my head of weeks if that was not there.

Fourth, go into the disco file replace the contractRef and both soap nodes with the following. Pay special attention to where you replace in the name of the service. I have bolded the things you must change based on the name of the web service – in this case InfoPathAttachment.

<contractRef ref=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request) + "?wsdl"),Response.Output); %>
docRef=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %>
xmlns="http://schemas.xmlsoap.org/disco/scl/" />
<soap address=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %>
xmlns:q1="http://tempuri.org/" binding="q1:InfoPathAttachmentSoap" xmlns="http://schemas.xmlsoap.org/disco/soap/" />
<soap address=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %>
xmlns:q2="http://tempuri.org/" binding="q2:InfoPathAttachmentSoap12" xmlns="http://schemas.xmlsoap.org/disco/soap/" />

Fifth, in the WSDL file, go to the every end of the file and you will find the following soap nodes.

<soap:address location="http://localhost/foo/InfoPathAttachment.asmx" />

<soap12:address location="http://localhost/foo/InfoPathAttachment.asmx" />

Change them to be:

<soap:address location=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> />

<soap12:address location=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(SPWeb.OriginalBaseUrl(Request)),Response.Output); %> />

DONE - That is really as simple as it gets!

4.0 Deploying the Service

Next we need to deploy this web service. I was disappointed in the MSDN Article because instructions had you go directly into the files of the server and dropping files on manually. In this blog, the author had you putting DLLs in both the GAC and the web bin directory. Nuts!

As I have said before, if I cannot deploy using a SharePoint solution you should probably reconsider your deployment plan. Plus, I have to consider the fact that if I have a SharePoint farm, I will have to manually put the files up on each server which is a drag. In this section I will show you how to create a WSP solution that you can use to deploy your custom SharePoint web service. Note, was not simple and required me to refresh myself of CAS.

It is a well known best practice to deploy all of your DLLs to the web bin directory (i.e. C:\Inetpub\wwwroot\wss\VirtualDirectories\80\bin). The reason why is:

  • Deploying to the GAC makes the DLL completely accessible. If you have a real SharePoint farm and you are trying to respect security some DLLs should not be accessible across all farms.
  • As well, may people get lazy and say just drop the DLL in the GAC or worse, make the security level medium or full. If anyone tells you that – they are not thinking straight. Not production environment should run above minimum unless a CAS policy has been specifically applied to a DLL.

4.1 Sign both Projects


You will need to sign both the web service and the utilities libraries. Even though they will not be deployed to the GAC, to write a CAS Policy we will need them to be signed to ensure they are uniquely named.

4.2 APTC the WSSDistillery.Utilities Project

When I was going through the deployment steps farther down I started to get errors, specifically when making the web service call. The errors were saying that partially trusted callers are not permitted. What was basically occurring was the WSSDistillery.Utilities class does not fully trust the web service call. This would occur because the web service DLL is in the SharePoint web site bin directory and not in the GAC. Even if I put the WSSDistillery.Utilities in the GAC, I would still get this error. The solution is to add the following into the AssemblyInfo .cs file of the WSSDistillery.Utilities project.

[assembly: System.Security.AllowPartiallyTrustedCallers]

I did some reading and found out that Microsoft.SharePoint allow partially trusted callers too; then I did not feel too bad. The deal is that make sure you are not susceptible to injection if you are going to allow this.

4.3 Manifest File


Next I create the following manifest.xml file which will deploy the DLLs and the .asmx and .aspx files to the ISPI folder. You do not need to create any sort of SharePoint Feature because all we are doing is pushing files on to the web servers. I am going to provide you with two options and this will work in a production environment that is running under minimal trust.

Before going continuing it would be good to open a Visual Studio command prompt and run the following commands as you will need these values when setting up the CAS policies:

sn -Tp C:\WSSDistillery\Utilities\WSSDistillery.Utilities.Service.InfoPathAttachment\bin\WSSDistillery.Utilities.Service.InfoPath.dll

sn -Tp C:\WSSDistillery\Utilities\WSSDistillery.Utilities.Service.InfoPathAttachment\bin\WSSDistillery.Utilities.dll
4.3.1 Option 1

In this option I deploy on DLL to the GAC and one to the web bin. What we are doing is allowing the web service to run in a safe mode.

<Solution xmlns="http://schemas.microsoft.com/sharepoint/"
SolutionId="2583E0DD-2B0E-41b4-BFF3-4D4100B3D11B">
<Assemblies>
<Assembly Location="WSSDistillery.Utilities.dll" DeploymentTarget="GlobalAssemblyCache" />
<Assembly Location="WSSDistillery.Utilities.Service.InfoPath.dll" DeploymentTarget="WebApplication">
<SafeControls>
<SafeControl Assembly="WSSDistillery.Utilities.Service.InfoPath.InfoPathAttachment, WSSDistillery.Utilities.Service.InfoPath, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7e600b50acc43694"
Namespace="WSSDistillery.Utilities.Service.InfoPath"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
</Assemblies>
<RootFiles>
<RootFile Location="ISAPI\InfoPathAttachment.asmx"/>
<RootFile Location="ISAPI\InfoPathAttachmentdisco.aspx"/>
<RootFile Location="ISAPI\InfoPathAttachmentwsdl.aspx"/>
</RootFiles>
<CodeAccessSecurity>
<PolicyItem>
<Assemblies>
<Assembly PublicKeyBlob="[ADD VALUE]"/>
</Assemblies>
<PermissionSet class="NamedPermissionSet" Name="WSSDistillery.Utilities.Service.InfoPath" version="1" Description="Permission for WSSDistillery.Utilities.Service.InfoPath">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Flags="Execution" />
<IPermission class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" Unrestricted="True" />
</PermissionSet>
</PolicyItem>
</CodeAccessSecurity>
</Solution>

4.3.2 Option 2


In this option I deploy both DLLs to the web site bin directory.

<Solution xmlns="http://schemas.microsoft.com/sharepoint/"
SolutionId="2583E0DD-2B0E-41b4-BFF3-4D4100B3D11B">
<Assemblies>
<Assembly Location="WSSDistillery.Utilities.dll" DeploymentTarget="WebApplication">
<SafeControls>
<SafeControl Assembly="WSSDistillery.Utilities.Folder, WSSDistillery.Utilities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5695a828a53b1107"
Namespace="WSSDistillery.Utilities"
Safe="True"
TypeName="*"/>
<SafeControl Assembly="WSSDistillery.Utilities.InfoPathForm, WSSDistillery.Utilities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5695a828a53b1107"
Namespace="WSSDistillery.Utilities"
Safe="True"
TypeName="*"/>
<SafeControl Assembly="WSSDistillery.Utilities.Items, WSSDistillery.Utilities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5695a828a53b1107"
Namespace="WSSDistillery.Utilities"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
<Assembly Location="WSSDistillery.Utilities.Service.InfoPath.dll" DeploymentTarget="WebApplication">
<SafeControls>
<SafeControl Assembly="WSSDistillery.Utilities.Service.InfoPath.InfoPathAttachment, WSSDistillery.Utilities.Service.InfoPath, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7e600b50acc43694"
Namespace="WSSDistillery.Utilities.Service.InfoPath"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
</Assemblies>
<RootFiles>
<RootFile Location="ISAPI\InfoPathAttachment.asmx"/>
<RootFile Location="ISAPI\InfoPathAttachmentdisco.aspx"/>
<RootFile Location="ISAPI\InfoPathAttachmentwsdl.aspx"/>
</RootFiles>
<CodeAccessSecurity>
<PolicyItem>
<Assemblies>
<Assembly PublicKeyBlob="[ADD VALUE]"/>
</Assemblies>
<PermissionSet class="NamedPermissionSet" Name="WSSDistillery.Utilities" version="1" Description="Permission for WSSDistillery.Utilities.Service.InfoPath">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Flags="Execution" />
<IPermission class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" Unrestricted="True" />
<IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS" />
</PermissionSet>
</PolicyItem>
<PolicyItem>
<Assemblies>
<Assembly PublicKeyBlob="[ADD VALUE]"/>
</Assemblies>
<PermissionSet class="NamedPermissionSet" Name="WSSDistillery.Utilities.Service.InfoPath" version="1" Description="Permission for WSSDistillery.Utilities.Service.InfoPath">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Flags="Execution" />
<IPermission class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" Unrestricted="True" />
</PermissionSet>
</PolicyItem>
</CodeAccessSecurity>
</Solution>

4.4 Create a ddf File


Second you will need to create .ddf file, with the following commands. I usually just call this WSP.ddf and I add it to my web service project. This will package up the two DLLs and the three web service files.


.OPTION Explicit
.Set CabinetNameTemplate="WSSDistillery.Utilities.Service.InfoPath.wsp"
.Set DiskDirectory1="C:\ "

manifest.xml
%outputDir%WSSDistillery.Utilities.dll
%outputDir%WSSDistillery.Utilities.Service.InfoPath.dll

.Set DestinationDir="ISAPI"
InfoPathAttachment.asmx
InfoPathAttachmentdisco.aspx
InfoPathAttachmentwsdl.aspx

; if we don't delete this variable, we get an error.
.Delete outputDir

4.5 Project Post Build Events


Then I add the following to the post build events of the Web Service project which will build the WSP every time I have successful build of the web service project.

cd $(ProjectDir)
MakeCAB /D outputDir=$(OutDir) /f "WSP.ddf"

4.6 Deployment Scripts

Now all you need to do is running the following STSADM command to push the solution out. Note you have to provide the URL of the web server because the DLLs are deployed to the web site bin and not the GAC.

stsadm.exe -o addsolution -filename C:\WSSDistillery.Utilities.Service.InfoPath.wsp
stsadm.exe -o deploysolution -url http://MyServer/ -name WSSDistillery.Utilities.Service.InfoPath.wsp -allowGacDeployment -allowCasPolicies -immediate -force
stsadm.exe -o execadmsvcjobs

Now test it, simply go to http://MyServer/_vti_bin/InfoPathAttachment.asmx and you will see the new web service.

4.7 Changes to Custom Web Service

If you make changes to the custom web service, like adding a new method or parameter to your WebMethod you will need to regenerate the WSDL that was described earlier. Then you will need repackage everything including the recompiled DLLs and push all of the changes back out using the WSP solution.


stsadm.exe -o retractsolution -url http://MyServer/ -name WSSDistillery.Utilities.Service.InfoPath.wsp -immediate
stsadm.exe -o execadmsvcjobs
stsadm.exe -o deletesolution -name WSSDistillery.Utilities.Service.InfoPath.wsp

4.8 Making Web Service Discoverable in Visual Studio

This is completely optional, however the MSDN Article mentions that to make the web service discoverable in Visual Studio as a web service alongside the default Windows SharePoint Services web services you need to modify spdisco.aspx file that is located in the ISPI folder in the 12 hive. You will need to make this change on every server in the farm.


<contractRef ref=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(spWeb.Url + "/_vti_bin/InfoPathAttachment.asmx?wsdl"), Response.Output); %>
docRef=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(spWeb.Url + "/_vti_bin/InfoPathAttachment.asmx"), Response.Output); %>
xmlns=" http://schemas.xmlsoap.org/disco/scl/ " />
<discoveryRef ref=<% SPHttpUtility.AddQuote(SPHttpUtility.HtmlEncode(spWeb.Url + "/_vti_bin/InfoPathAttachment.asmx?disco"),Response.Output); %>
xmlns="http://schemas.xmlsoap.org/disco/" />

5.0 Using in InfoPath

I am not going to dive much farther to InfoPath development at this point. If you can get these methods created, you should be good to go from here. Below are a few notes to get you going.

5.1 Web Service Data Connection

Hooking this into you InfoPath form is really straight forward. All you need to do is create a data connection using a web service to http://MyServer/_layouts/InfoPathAttachment.asmx. When creating that web service data connection, do not select submit option, make sure you select retrieve option. I am going to skip the rest of the configuration of the web service data connection as that should be pretty simple.


5.2 Create an Upload Button

The only other little trick you will need to know is that on your InfoPath form, you will need to add a binary data field (base64) into the main source. The reason why is if you try to use the binary field in web service data connection on the form, InfoPath will not allow you to use the attachment control. Now I bet if you crack open the manifest.xsf file you can probably resolve this issue.

Below is a screen shot of a rule I created on a button that will upload a file from the IP from to the SharePoint server. Now all you need to do is create a rule to clear out the main data source attachment field and then create a third rule that will call the GetAttachments web method.

With all of these web methods you could satisfy a use case like:

  • User opens a Purchase Request InfoPath form.
  • A Purchase Request number is generated in the InfoPath form (out of scope of this posting).
  • An attachment folder is created for the Purchase Request.
  • User uploads a file associated to the Purchase Request.
  • User will see links Purchase Request attachments on the form.

5.3 SharePoint Farm?

If you SharePoint farm that has several front-end web servers that are load balanced, you may run into an issue when making a connection to SharePoint web service when the InfoPath form is running in a web enabled mode. I ran into this issue a long time ago when trying to use the SharePoint Profile web service to get the current users information when using a web enabled InfoPath form (read this blog). You will need to use the URL to the web server directly instead of the load balanced URL. I have never dug any deeper into why this; if you scroll down in the responses of the blog you will see the issue that everyone runs into.

6.0 References


I did get some help along the way:

2 comments:

Myke said...

Hi... I need some help with this. I am getting an error when calling the webservice:

The query cannot be run for the following DataObject: UploadInfoPathAttachment
InfoPath cannot run the specified query.
The SOAP response indicates that an error occurred on the server:

Server was unable to process request. ---> Error saving attachment >> There is no Web named "/idivauth".

my site is https://sitename.com/sites/idivauth

so i set "siteURL = https://sitename.com" and "webUrl = idivauth" from the InfoPath rule.

Jason Apergis said...

Myke,

Not sure if your understand what you are talking about. I have not looked at this code for some time. I know it worked 100% fine when I deployed. I say take a second look.

Jason