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:

Thursday, February 5, 2009

February K2 User Groups

There are two K2 user groups meetings for the month of February which you can attend virtually…

The second one has already passed but the recording should be available on the K2 Underground.


February 10, 11am-1pm central time – K2 blackpoint Preview

Phillip Knight from Merit Energy will be hosting the K2 user group meetings at Merit Energy, located at 13727 Noel Road, 2nd Floor Conference room, Tower 2, Dallas, Texas 75240. Parking information is included in the linked map below. Remote attendance information is included at the bottom of this message.

Link to map: http://www.meritenergy.com/content/MeritMap.pdf. Reminder: Merit Energy is on the 5th floor, but the meeting will be held in a 2nd floor conference room. Once off the elevator, go to the reception area and we will bring you back to the conference room.

If you haven't already done so, please RSVP to me via email
whether you are attending via live meeting or if you will be attending in person (so that we can plan for the number of people to order food for).

Check out the K2 Underground site and our user group at http://k2underground.com/k2/InterestGroupHome.aspx?IntGroupID=11. We are posting webexes/live meetings from our meetings at this site.

Meeting Agenda:
11-11:15 Networking/Refreshments
11:15-11:30 Announcements/Intros of New people
11:30-11:45 Tips & Tricks
11:45-12:45 Technical Presentation
12:45-1:00 Meeting Wrapup

The Announcements section of the meeting will include any information regarding K2 upcoming events and user group events as well as brief introductions of our presenter and refreshment provider.

The Tips & Tricks Presentation is when we as members can pose questions to each other on projects that we are working on and having difficulty with. It is also a time when if we have learned something that we feel will be helpful to others, we can share it with the group. Bring yours to share/ask.

Meeting Presentation & Company:

Michelle Salazar from K2 will be presenting on K2 BlackPoint. K2 blackpoint, a subset of K2 blackpearl features, provides unparalleled capabilities and affordability. It also offers an upgrade path so that organizations can grow their investment and add complexity over time, if needed. Michelle will demonstrate to us the features available in K2 and the ease of use in developing workflows with it.

For more information, go to http://www.blackpoint.k2.com.

The K2 platform is for delivering process-driven applications that improve business efficiency. Visual tools make it easy for anyone to assemble reusable objects into applications that use workflow and line-of-business information.

K2-based solutions are deployed by a growing number of the global Fortune 100. K2 is a division of SourceCode Technology Holdings, Inc. based in Redmond, Washington, and has offices all over the world.

For more information, contact Joe Bocardo at joeb@k2.com.

Meeting Presenter:

Michelle Salazar

Michelle Salazar resides in Dallas, TX and before joining K2 she held a variety of positions in the information technology field. Her primary focus has been on developing enterprise-class Windows and web-based applications using Microsoft technologies.

Meeting Sponsor:

We thank Michael Steinberg from Pariveda Solutions for sponsoring our refreshments at our August meeting.

Pariveda Solutions is a mid-market IT Consulting firm with offices in Chicago, Dallas, Denver, Detroit, Houston, and Seattle. We are in the Business of IT®. We empower Information Technology organizations to function more like services companies inside their corporate enterprises. From the way they define their internal strategy, to the way they deploy their resources and provide value to the business, our proven methodology drives costs out of IT while improving internal service quality and responsiveness. This organizational transformation is necessary and foundational for any IT enterprise engaging in the Business of IT®.

For more information please contact
Michael Steinberg, Pariveda Solutions (michael.steinberg@parivedasolutions.com www.parivedasolutions.com (214) 557-0558)

For more information

There is another K2 User Group in Denver, Colorado led by Paul DeFrees. They meet the 1st Tuesday of every month from 6pm – 8pm Mountain time or 7pm-9pm Central time. If you would like to know more about their user group and would like to receive meeting announcements, please email Paul DeFrees at paul.defrees@baxa.com. Their next meeting is February 3rd and one of their members will be presenting a K2 BlackPearl application.

For Virtual Attendees:

Note: please keep your phone on mute until you are ready to speak.

Audio Information

Telephone conferencing
Choose one of the following:

Start Live Meeting client, and then in Voice & Video pane under Join Audio options, click Call Me. The conferencing service will call you at the number you specify. (Recommended)

Use the information below to connect:
Toll-free: +1 (877) 860-3058

Toll: +1 (719) 867-1571

Participant code: 914421

First Time Users:

To save time before the meeting, check your system to make sure it is ready to use Microsoft Office Live Meeting.
Troubleshooting
Unable to join the meeting? Follow these steps:

Copy this address and paste it into your web browser:

1. https://www.livemeeting.com/cc/scna1/join?id=Z2RP5R&role=attend&pw=TBK%3BWj37j

2. Copy and paste the required information:
Meeting ID: Z2RP5R

Entry Code: TBK;Wj37j
Location: https://www119.livemeeting.com/cc/scna


February 3, 6 to 8pm central time – Underwriting Solution

The next K2 User Group meeting in Denver will be held today, February 3rd, from 6pm to 8pm MST.

The meeting will again be hosted at Baxa located at 14445 Grasslands Dr, Englewood, CO 80112. Here is a map to the Baxa office. There is plenty of parking available around the building and it's fairly close to E470, so please join us in person, if possible. For those of you who cannot attend in person, we will have a Live Meeting session available. This information is located at the bottom of this e-mail.

If you are interested in receiving these notifications in the future, please register for the Denver User Group on the K2 Underground. If you would prefer to not be contacted again, please let me know so I can remove your name from the list.

Agenda:

6:00 - 6:15 Networking/Refreshments
6:15 - 6:30 Announcements
6:30 - 7:45 Presentation by Scott Daub from Imerica
7:45 - 8:00 Meeting Wrap-up

Presentation Summary:

This month, Scott Daub, a K2 customer with Imerica, will present the workflow he implemented at his company to manage their underwriting process. The Underwriting process is the core decision making process in their business. They had a need to rapidly build a replacement to the existing system. The presentation will walk through the initial implementation and how it evolved over four months. He will talk about some the lessons learned and the pain points of the development cycle as well as some plans for the future of this process.

Scott began working with K2 about five years ago as a consultant and has used it on a variety of projects. He joined Imerica Life and Health Insurance in June as a Lead Developer where he implemented the underwriting process using K2 and has big plans for using K2 in other areas of the company.

As always, bring your K2 or workflow questions for discussion with the group and get help from those have been there or who are working on similar processes or challenges. Anthony will help you work through your design scenarios or issues, so bring your tough questions!

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…

K2 blackpearl and MetaStorm Comparison

There may be time where a company or organization will start looking for enterprise human workflow solution. A product like K2 blackpearl may be considered. There are other products out there like Adobe Forms Server, MetaStorm but specifically I will focus on MetaStorm and K2 blackpearl.

MetaStorm is a product that focuses on Business Process & Analysis (BPA) and Business Process Management (BPM), however K2 blackpearl focuses on BPM to build Process Driven Application. Hopefully by the end of this posting I will make this clear and what is the difference.

Note I did have the opportunity to sit in a MetaStorm demo, review some materials and was able to ask some questions. My knowledge of MetaStorm is no where near as deep as my knowledge of K2. This was the impressions that I walked away with and I will give both technologies a fair shake.

Both products have several similarities:

  • Much of the vocabulary between the products is the same. For instance there are Actions, Dynamic Rules, Activities, Events, Processes, Rules, and the list just goes on.
  • Both provide the ability to graphically model business workflows and to create execution models from them.
  • Both provide the ability to handle medium and complex workflows.
  • Both provide the ability to utilize line of business data across the enterprise.
  • Both provide governance, metrics and business intelligence into the performance of the automated business process.
  • Both provide monitoring and administrative tools to manage processes.
  • Both provide the ability for strong Information Workers to author workflows.
  • Both provide declarative models such that deployed business processes can be changed on the fly.
  • Both provide the ability to access external data to make business decisions.
  • Both provide the ability to manage content and documentation.

The degree by which K2 and MetaStorm supports these feature probably varies; one may do something better than the other but you get the point. Regardless, it is this sort of functionality that will drive you down a path for procuring a workflow product because you are tired of building this from scratch.

Great. After sitting through two great demos, and you will be asking what is the difference between these two technologies? There are some Gartner reports out there, and they go down to the level of trying to compare two. Not to say I do not agree with their analysis (some of the analysis, I am not sure is needed) but really get the impression the analysis was done by people without field experience.

Here are some differentiators for MetaStorm:

  • Currently provides strong designer tools for a trained business user to author simple step approval processes.
  • Provides a mature forms generation tool.
  • Provides enterprise tools for doing enterprise planning and business process analysis and how they relate to automated business processes.
  • Provides the ability to use either SQL Server or Oracle as its server database.
  • Provides more native access to the Java Platform.

Here are some differentiators for K2 blackpearl:

  • Augments the Microsoft application server stack: MOSS, BizTalk and SQL Server.
  • Provides the ability to leverage the MOSS platform to deliver MOSS solutions.
  • Provides ability for lower level of customizations as the K2 blackpearl platform is built upon frameworks by Microsoft (Windows Workflow Foundation).
  • Provides better ability to do complex database integration out of the box.
  • K2 blackpoint provides an Information Worker environment for building medium level complexity workflows hosted in SharePoint.

Reality is:

  • Both these tools provide for better business user participation and tools for developers to create implementations quickly.
  • There is no such thing as a simple workflow. Coders or engineers will always be required to build and manage the processes such that they can scale to the evolving business processes and technologies. I understand the market place is pushing towards this and this is where the battleground is. Forms generation can be done through various tools, but at the end of the day (given my experiences) there is some wiring up the forms and business users are not data architects. I really believe that with products like K2 blackpearl or MetaStorm are used, organizations can spend more time focusing on automating their business rules rather than trying to figure out how to manage transactions…

When it comes down to it:

  • MetaStorm is a forms server; that is their roots. K2 blackpearl does not have a strong forms generation tool. Right now, K2 uses InfoPath as it forms tools that Information Workers can use. InfoPath is not that bad if you understand it limitations. However I had the impression that MetaStorm's form generation was proprietary. Both tools do provide the ability to use other platforms for forms. Both can use ASP.net, JSP, whatever for their forms.
  • MetaStorm have made an investment in making their application server available through SharePoint. However the problem is that if you have a major investment in SharePoint or MOSS, MetaStorm does not help with the automation of content within SharePoint. All MetaStorm does is open their forms tool through SharePoint and they provide dashboard web parts. K2 blackpearl provides wizards to do everything a user can do to a list item, calendar, document item, etc. K2 blackpearl provides functionality to provision user permissions, site management, publishing pages, etc. All K2 blackpearl data can be exposed through SharePoint in the same manner. It is important to note that K2 blackpearl is NOT only good for SharePoint. We have used K2 to automate business processes that have nothing to do with SharePoint.
  • Both K2 blackpearl and MetaStorm have data integration but in the presentation I saw of MetaStorm I was left believing K2 blackpearl has an advantage with its SmartObject framework. MetaStorm did provide the ability to call out to a web service, stored procedure, or custom code but that is what is expected. K2 SmartObjects really take it to the next level where you can look at business data holistically and define business objects that are not coupled to the business process implementation. Plus there is K2 Connect which can access SAP and they plan to use it framework to access other enterprise servers. MetaStorm does provide data adapters to access enterprise data however they needed to be purchase individually and I did not get the impression they were open to developers to customize.
  • MetaStorm provides this entire business process analysis tool which I did not get to dive into. Basically from what I can glean from their web site and conversations I had, they provide tools where you can capture company initiatives, goals, objects, strategic information, etc. model that with your enterprise and then create executable business models from them. Pretty neat to discuss and K2 blackpearl does not provide this. However, I have not seen lots of demand for this either when working with many of my clients in the past (K2 or not).
  • K2 blackpearl is accessible from a lower level and is a more customizable. MetaStorm's advantage, stated above, is that they do cater to the users who are less technical empowering them to create forms and simple workflows. With K2 blackpoint coming out, K2 has bridged that gap however K2 blackpearl requires a technical person because the best solutions are built in Visual Studio. I believe this to be the advantage of the K2 blackpearl platform – and why I would choose it each time. With K2 blackpearl, I have the ability to go in and do what I want to get the job done.
  • I cannot discuss licensing (even though I got some insight into MetaStorm's in the discussion), however I can say both are different and scale differently from a licensing perspective. What I can say is K2 blackpoint, which is the K2's lighter weight workflow server, is only $10K for doing SharePoint workflow.

To boil this down to an elevator conversation:

  • MetaStorm is a mature forms generation server product that provides the ability to create workflow for forms that reside in their server (and content that resides in external systems).
  • K2 blackpearl provides a platform to build process driven applications regardless of the platform and has a deep integration into the Microsoft application server stack (especially SharePoint, BizTalk, ASP.net, InfoPath, SQL Server, etc.).