Unknown's avatar

Posts by Simon Doy

I am an avid SharePoint enthusiast who works as an Independent SharePoint Consultant based in Leeds, United Kingdom. I am one of the organisers of the Yorkshire SharePoint User Group in the United Kingdom. I have been designing and building SharePoint solutions since 2006.

Issue applying Workflow Associations to Content Types


 

Introduction

 

First, let’s set the scene. We have built a solution for a customer with a site collection and within that site collection a sub-web which has a large number of document libraries in it.

For the next release we were looking to setup records management and use the out of the box disposition workflow to allow the customer to decide whether to delete the document or not. The workflow is triggered by an information policy.

The upgrade went well all the feature upgrades worked nicely however then we hit a snag setting up the workflows.

The process of applying a workflow to a content type is pretty straight forward and we did not have any problems with the development, staging or UAT environments. Of course its only when we go to setup in production did we hit a problem.

Applying the workflow to a content type was achieved by doing the following:-

  • Browse to the root of your site collection
  • Click on Site Content –> Settings
  • Click on Site Content Types
  • Click on your content type that you wish to configure
  • Click “Workflow Settings”
  • Click Add a Workflow which takes you into the following screen

addingworkflowtocontenttype

If you look at the image at the bottom of the page you will see the option “Update all content types”, so we set this and clicked Ok.

The page just hung there processing, we left it for a while but still it was hung there. Knowing SharePoint we thought that is fine we’ll leave it. Two hours later still nothing so we started to check the SharePoint Unified Logs and unfortunately we found an error

 

SharePoint Foundation   Database        fa46    High    at Microsoft.SharePoint.SPSqlClient.ExecuteQueryInternal(Boolean retryfordeadlock)     at Microsoft.SharePoint.SPSqlClient.ExecuteQuery(Boolean retryfordeadlock)     at Microsoft.SharePoint.Library.SPRequestInternalClass.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, Boolean bPrefetchRelatedFields, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)     at Microsoft.SharePoint.Library.SPRequestInternalClass.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, Boolean bPrefetchRelatedFields, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)     at Microsoft.SharePoint.Library.SPRequest.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, Boolean bPrefetchRelatedFields, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)     at Microsoft.SharePoint.SPListCollection.EnsureListsData(Guid webId, String strListName)     at Microsoft.SharePoint.SPListCollection.GetListByName(String strListName, Boolean bThrowException)     at Microsoft.SharePoint.Workflow.SPWorkflowAssociation.EnsureUtilityList(SPWeb web, String workflowName, String listTitle, Guid Id, SPListTemplateType templateType, Boolean forceListCreation, String alternateNameRes, String descriptionRes)     at Microsoft.SharePoint.Workflow.SPWorkflowAssociation.EnsureTaskList(SPWeb web, String workflowName, String createTaskListTitle, Guid createTaskListGuid, Boolean forceListCreation)     at Microsoft.SharePoint.Workflow.SPWorkflowAssociationCollection.SetUtilityLists(SPWorkflowAssociation wa, Boolean forceUtilityListCreation)     at Microsoft.SharePoint.Workflow.SPWorkflowAssociationCollection.AddCore(SPWorkflowAssociation wa, Guid id, SPList list, Boolean forceUtilityListCreation)     at Microsoft.SharePoint.Workflow.SPContentTypeWorkflowAssociationCollection.AddCoreCT(SPWorkflowAssociation wa)     at Microsoft.SharePoint.Workflow.SPContentTypeWorkflowAssociationCollection.PushDownAssociation(SPWorkflowAssociation associationTemplate, Boolean bUpdateIfExisting, MethodBase mbChangeEntry)     at Microsoft.SharePoint.Workflow.SPContentTypeWorkflowAssociationCollection.UpdateOrAdd(SPWorkflowAssociation associationTemplate)     at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)     at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)     at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)     at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)     at Microsoft.SharePoint.SPChangeMonitor.ApplyChangesCore(Object ct, Boolean applyAll, Type typeFilter, Boolean bFilterInclude)     at Microsoft.SharePoint.SPContentType.PushDownWorkflowChangesToDerivedCTCore(SPContentType ct, Boolean fCloseWebAsNecessary)     at Microsoft.SharePoint.SPContentType.PushDownWorkflowChangesToListCTs(SPContentTypeCollection cts, IEnumerable`1 cids, Boolean throwOnSealedOrReadOnly)     at Microsoft.SharePoint.SPContentType.PushDownChanges(CodeToPushDownChangesToDerivedCT derivedCTPushdownCode, CodeToPushDownChangesToListCTs listDerivedCTsPushdownCode, Boolean throwOnSealedOrReadOnly, IList`1 exceptions)     at Microsoft.SharePoint.SPContentType.UpdateWorkflowAssociationsOnChildren(Boolean bGenerateFullChangeList, Boolean bPushdownDerivedCT, Boolean bPushdownListCTs, Boolean bThrowOnSealedOrReadOnly)     at Microsoft.SharePoint.WorkflowServices.ApplicationPages.WrkSetngPage.OnClick_Update(Object sender, EventArgs e)     at System.Web.UI.WebControls.LinkButton.RaisePostBackEvent(String eventArgument)     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest()     at System.Web.UI.Page.ProcessRequest(HttpContext context)     at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)     at System.Web.HttpApplication.PipelineStepManager.ResumeSteps(Exception error)     at System.Web.HttpApplication.BeginProcessRequestNotification(HttpContext context, AsyncCallback cb)     at System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)     at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)

 

The reason for this error was explained in the next log entry which was a little more concise:-

Getting Error Message for Exception System.Web.HttpException (0x80004005): Request timed out

 

So a bit of head scratching and we thought well we should really make this change via PowerShell but the problem is how to do that?

 

Solution

 

Well its funny some of the method calls you see on SharePoint objects. Obviously the guys at Microsoft have seen this issue before. When I was updating the SPContentType I remember seeing the function UpdateWorkflowAssociation() but then there was the much more useful UpdateWorkflowAssociationsOnChildren()!

Thanks to the team at Microsoft – that is just what we were looking for!

The fix was the following piece of PowerShell:-

 

$web = Get-SPWeb [your site collection root web url]; $contentTypeToUpdate = $web.ContentTypes | ?{$_.Name –eq “[Your Content Name Here]”}; $contentTypeToUpdate.UpdateWorkflowAssociationsOnChildren($true, $true, $true, $false);

I used ULSViewer to watch the function running SPSqlClient sessions to see that the process was working correctly and more importantly it wasn’t timing out!

 

To talk through the PowerShell we are doing the following:-

  • Getting the SharePoint web for the root of our site collection which contains the content type to update
  • Get the content type that we need to update
  • Call the UpdateWorkflowAssociationsOnChildrenUpdate()
    • mark all the content as changed = true
    • push down to all derived content types = true
    • push down to all content types that are associated to lists = true
    • throw an error if you encounter a read-only or sealed content type = false

Lessons Learned

 

When you are doing upgrades always try and use a similar sized data set as production!

When it does not work through the UI – bring on the PowerShell!

 

Anyway I hope that helps someone else get themselves out of trouble!

Experiences with SharePoint Disposition Approval Workflow


 

Introduction

 

Recently I have been working on document lifecycle management project. One of the targets for the project was to try and use out of the box (OOB) SharePoint features rather than develop additional components. I am all for this, it reduces support and maintenance overhead and the technical debt of the solution.

The solution ended up implementing a multiple content types with information management policies for auditing and retention policies.

The retention policy had a number of stages which were:-

  • Created date + 1 day make the document into a record
  • Created date + X years execute the “Disposition Approval Workflow”

The number of years would vary depending on the type of document.

Before I go any further I should talk about the Disposition Approval Workflow, this is a workflow which has been in SharePoint since SharePoint 2007 and is made available using the Disposition Workflow feature.

The workflow when run against a document has one step. The step creates a task which presents the user with the option to either delete the document or retain it. The user can also provide comments.

What should happen is that if the user chooses to delete the document then the workflow will delete the document.

However, this fails if the document is declared as a record. Unfortunately the out of the box disposition approval workflow which has been designed to provide a mechanism to delete a document cannot delete a document if its a record.

The workflow displays the following error or words to the effect of “Cannot delete the document”

This was a little frustrating so what do we do?

 

Options

 

After some thought about the problem I came up with two approaches:-

  • redesign the Disposition Approval Workflow to work how we want it to
  • add a step to undeclare the document as a record

Thinking back to one of the key targets, keep the amount of custom development to a minimum, I decided to go for the second option.

Unfortunately, there is no expiration action which allows you to undeclare a record. So I looked at the options to create one.

Fortunately there are a couple of reasonable examples, they might be a little lacking in detail but there is enough to understand the process.

 

Solution

 

So the following solution was built, the solution is made up of the following parts:-

  • A class which implements the IExpirationAction interface and perform the action to undeclare a document as a record
  • A feature which has a feature receiver which adds the IExpirationAction to the PolicyTemplates
    A class UndeclareRecordExpirationAction is created which implements the IExpirationAction interface as follows:-
    The IExpirationAction interface has a function OnExpiration which passes in the SPListItem to delete and the date that the document has expired on.
    The function implementation would be to check that the document is not already on hold and also that the document isn’t a record already.
    if the document is on hold then an exception is raised so that the process goes no further.

Two static functions were created which register / unregister the ExpirationPolicyAction with SharePoint.

 

A feature was created which implemented FeatureActivatation and FeatureDeactivation methods. These call into the appropriate static functions in the UndeclareRecordExpirationAction class.

 

Once the solution is installed and the feature activated then a new policy action appears here:

image

 

To configure the policy do the following:-

  • Browse to the site collection
  • Browse to the site settings page
  • Click Site Content Types
  • Click on the content type that you wish to apply the policy to
  • Click on “Information Management Policy settings”
  • Click “Define a policy..”
    • Click Ok
    • Click Enable Retention
    • Add a retention stage…

    The information policy was modified so that it has the following steps:-

    Created + 1 day = Declare record (reoccurrence 1 month)

 

    Created + 6 Years = Undeclare Record (reccurrence 1 year)

 

    Created + 6 Years = Start Workflow (reoccurence 1 year)

The option to specify how to manage retention on records was set that the same policy was used for both records and non-records.

I set the reoccurrence for the Declare record action to one month so that if a document was undeclared and then the record managers decided not to delete the document through the workflow it would then picked up again.

 

Testing the solution

The process of testing these expiration policies was a little tricky so I will cover the method that used. Information policies are applied by timer jobs. The following timer jobs are used:-

  • Information Management Policy Timer Job
  • Expiration Timer job

The expiration timer job does the actual work of applying/running the various steps of a retention policy. By default it runs once a week in SharePoint 2013.

To speed things up I did the following to test/debug the solution.

  • Compile the assembly hosting our UndeclareRecordExpirationAction class as a debug build.
  • Deploy the solution
  • Restart SharePoint Timer Service
  • Attach the Visual Studio debugger to the “owstimer.exe” process
  • From Central Admin, start the timer job through the monitoring->review job definintions
  • Browse to the Expiration Policy Job Definitions
  • Click on the “Expiration policy” link for the one associated to the Web Application where your application is hosted and click “Run now”
  • Wait for the visual studio debugger to hit the breakpoint

If you are having problems getting the debugger to hit the breakpoint then:-

  • Start the SharePoint Management Shell
  • From within the PowerShell session
  • restart the owstimer service using Restart-Service sptimerv4
  • restart IIS using iisreset

Improvements

 

The only thing that I may look to do is rather than have two stages for “Undeclare record” and “Starting a workflow” is merge them into two and have a “undeclare record and start workflow action” so that there is less delay with the processing.

 

Conclusion

 

I hope people find this useful, please let me know if you do.

 

Here are links to the code samples:-

ITSP.SP.Policy.UndeclareRecordAction.zip