After migration of a SharePoint 2007 application to SharePoint 2010 we encountered an unhandled exception occurring arbitrarily when a list item is opening for editing. The exception looks as follows:
Exception Details: System.ArgumentNullException: Value cannot be null.
Parameter name: s
Source Error:
An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.
Stack Trace:
[ArgumentNullException: Value cannot be null. Parameter name: s]
System.IO.StringReader..ctor(String s) +10151478
Microsoft.SharePoint.Publishing.Internal.WorkflowUtilities.FlattenXmlToHashtable(String strXml) +117
Microsoft.SharePoint.Publishing.Internal.WorkflowUtilities.DoesWorkflowCancelWhenItemEdited(String associationXml) +12
Microsoft.SharePoint.Publishing.WebControls.ConsoleDataSource.EnsurePageNotInLockingWorkflowIfInEditMode() +207
Microsoft.SharePoint.Publishing.WebControls.ConsoleDataSource.LoadDataSource() +199
Microsoft.SharePoint.Publishing.WebControls.ConsoleDataSource.OnLoad(EventArgs e) +98
Microsoft.SharePoint.Publishing.WebControls.XmlConsoleDataSource.OnLoad(EventArgs e) +201
Microsoft.SharePoint.Publishing.WebControls.PublishingSiteActionsMenuCustomizer.OnLoad(EventArgs e) +186
System.Web.UI.Control.LoadRecursive() +66
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Control.LoadRecursive() +191
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +2428
Having analyzed the stack trace we found out that the issue is, evidently, caused by workflow infrastructure. In the SharePoint application we have both handmade custom workflows and the ones that were made in SharePoint Designer (so called SPD Workflows). It’s interesting that the exception was rising for both workflow types. After some investigation using .Net Reflector, we discovered that the AssociationData xml-element is a cause of our troubles. The AssociationData is an element of the Workflow Definition Schema and specifies any custom data to pass to the workflow association form. Additionally, we use the association data when starting workflow on list items through the code. The workflow infrastructure, in SharePoint 2010, supposes that the AssociationData element is a valid xml string. So, if it’s empty or contains any data without xml-tags, the exception is thrown as opposite to SP 2007.
The solution consists of two steps. Firstly, for every Workflow Definition in your project you need to set <AssociationData> so that it contains a valid xml. Regardless of whether association data is actually used or not, <AssociationData> has to be presented in Workflow Definition and contain at least a fake valid xml. For example, I set dummy <Data />. In case you deploy a custom workflow through a SharePoint Feature, the Workflow Definition is usually located in Elements.xml file. If <AssociationData> element doesn’t exist within <Workflow> tag, add it at the same level as <MetaData>. The resultant workflow definition should look like:
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Workflow ...>
...
<AssociationData>
<Data />
</AssociationData>
<MetaData>
...
</MetaData>
...
</Workflow>
</Elements>
The described first step of the solution affects only new applications created after the alterations have been applied to Workflow Definitions. Even if a SharePoint Feature containing a custom workflow is reactivated, changes are only applied to workflow instances that start after the workflow association is modified. But for live applications created before or migrated from previous version of SharePoint (like in my case), the first step is useless. Additionally, you may have the SPD workflow which doesn’t include usual Workflow Definition at all. So, the second step is a necessary and sufficient part of the solution.
The second step is get all SPWorkflowAssociation objects and adjust their AssociationData properties programmatically. I’ve developed a few methods allowing to achieve this goal. See the listing below:
using System;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
...
/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed Web, its child Lists and Content Types
/// </summary>
public static void AdjustAllWorkflowAssociations(SPWeb web)
{
AdjustWebWorkflowAssociation(web);
for (int i = 0; i < web.ContentTypes.Count; i ++)
AdjustContentTypeWorkflowAssociation(web.ContentTypes[i]);
for (int i = 0; i < web.Lists.Count; i ++)
{
AdjustListWorkflowAssociation(web.Lists[i]);
for (int j = 0; j < web.Lists[i].ContentTypes.Count; j ++)
AdjustContentTypeWorkflowAssociation(web.Lists[i].ContentTypes[j]);
}
}
/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed List
/// </summary>
public static void AdjustListWorkflowAssociation(SPList list)
{
AdjustAssociationData(list.WorkflowAssociations);
}
/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed Web
/// </summary>
public static void AdjustWebWorkflowAssociation(SPWeb web)
{
AdjustAssociationData(web.WorkflowAssociations);
}
/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations related to the passed Content Type
/// </summary>
public static void AdjustContentTypeWorkflowAssociation(SPContentType contentType)
{
AdjustAssociationData(contentType.WorkflowAssociations);
}
/// <summary>
/// Adjusts AssociationData properties of all Workflow Associations in the passed collection
/// </summary>
public static void AdjustAssociationData(SPWorkflowAssociationCollection collection)
{
for (int i = 0; i < collection.Count; i ++)
AdjustAssociationData(collection[i], collection);
}
/// <summary>
/// Sets AssociationData property if it's not valid
/// </summary>
public static void AdjustAssociationData(SPWorkflowAssociation workflowAssociation, SPWorkflowAssociationCollection collection)
{
if (!IsValidXml(workflowAssociation.AssociationData))
{
string newValue = string.IsNullOrEmpty(workflowAssociation.AssociationData)
? "<Data />"
: string.Format("<Data>{0}</Data>", workflowAssociation.AssociationData);
workflowAssociation.AssociationData = newValue;
collection.Update(workflowAssociation);
}
}
/// <summary>
/// Checks if the passed string is a valid xml
/// </summary>
public static bool IsValidXml(string str)
{
if (!string.IsNullOrEmpty(str))
{
try
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(str);
return true;
}
catch {}
}
return false;
}
The basic method in the set is AdjustAssociationData, which examines AssociationData property of a passed SPWorkflowAssociation object and then applies a valid xml-value to the property if it’s necessary. As we know, workflows can be associated with webs, lists or content types. The AdjustWebWorkflowAssociation, AdjustListWorkflowAssociation and AdjustContentTypeWorkflowAssociation methods adjust AssociationData of a passed SPWeb, SPList or SPContentType object respectively. Finally, the AdjustAllWorkflowAssociations tries to adjust all available workflow associations that can be reached through a passed SPWeb instance.
Note that I everywhere used the for-statement instead of foreach, because otherwise the exception “Collection was modified; enumeration operation may not execute” will be thrown.
Below is the piece of code from the simple console application, which I used against our problem SharePoint application:
static void Main(string[] args)
{
try
{
AppArguments arguments = new AppArguments(args);
if(string.IsNullOrEmpty(arguments.Url))
{
Console.WriteLine("Invalid application URL!");
return;
}
using (SPSite spSite = new SPSite(arguments.Url))
using (SPWeb spWeb = spSite.OpenWeb())
AdjustAllWorkflowAssociations(spWeb);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Where the AppArguments is a class derived from InputArguments. InputArguments is described here – Simple Command Line Arguments Parser.
public class AppArguments : InputArguments
{
public string Url
{
get { return GetValue("url"); }
}
public AppArguments(string[] args) : base(args)
{
}
}
As for our SharePoint application, it has only workflows associated with lists. So, after executing of the console app described above, the exception in question is gone. I hope it’ll help somebody as well.
The full version of the console app you can download here (Visual Studio 2010 solution). To execute it use the command line with parameters as follows:
AdjustAssociationData.exe -url "http://yourservername/yourapppath"