Tuesday, May 26, 2009

Starting workflow programmtically from custom actions for users with readonly permissions

Recently was working with picture libraries, and had a scenario in which I was to start a workflow through a custom action. Additionally, the workflow should be started by all those users as well which do not have edit rights on the library for which workflow is started. So once have done it successfully, thought to post it up here.
First the Custom action part:
My custom action should appear on the ECB menu and edit toolbar of item in the library. For this I first create a feature.xml file which appears as:

<feature id="" title="Picture library Custom Action" xmlns="http://schemas.microsoft.com/sharepoint/" scope="Web">
<elementmanifests>
<elementmanifest location="UICustomActions.xml">
</elementmanifests></feature>


The scope of the above feature is web since, the custom action cannot be activated on site collection or web application level. Id would be a GUID generated through the CreateGUID tool.

The code for element file UICustomActions.xml is as follows:
<elements xmlns="http://schemas.microsoft.com/sharepoint/">
<customaction id="PicturelibraryFeature.StartWorkflow" title="Start Workflow" location="EditControlBlock" registrationtype="ContentType" registrationid="0x0101020047370705EAAA49c0B6E294964FE2D049" sequence="106" imageurl="~site/_layouts/images/imageAction.png">
<urlaction url="~site/_layouts/CustomPictureLib/WorkflowStart.aspx?List={ListId}&ItemId={ItemId}">
</customaction>
<customaction id=" PicturelibraryFeature.StartWorkflow" title="Start Workflow" location="EditFormToolbar" registrationtype="ContentType" registrationid="0x0101020047370705EAAA49c0B6E294964FE2D049" sequence="106" imageurl="~site/_layouts/images/imageAction.png">
<urlaction url="~site/_layouts/ CustomPictureLib/WorkflowStart.aspx?List={ListId}&ItemId={ItemId}">
</customaction></elements>


Above are two sections of Custom actions, one is for the ECB menu and another is for Edit tool bar. The attributes in the Custom action are clear by themselves. My aspx page is located in _layouts folder of the webserver.

To deploy these features, I create a folder at the following location C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES called “PicturelibraryFeature” and place both the xml files in it. Next comes installing and activating the feature through STSADM commands:

stsadm -o installfeature -filename PicturelibraryFeature \Feature.xml
stsadm -o activatefeature -filename PicturelibraryFeature \Feature.xml -url http://URL


The activation can be done through UI too.

Now the aspx page that is target url for our custom action, I call it WorkflowStart.aspx, it will be placed in a folder “CustomPictureLib” at the following location C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS of your web server.

This aspx page is a site page, so I include it in my project and also include a class file WorkflowStart.cs as its code behind.

The following is code for WorkflowStart.aspx:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WorkflowStart.cs" Inherits="CustomPictureLibrary.WorkflowStart, DIAPhotolibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=4a78f6d69c6311d0" %>

<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<?xml:namespace prefix = asp /><asp:content runat="server" contentplaceholderid="PlaceHolderMain">
<h3>Confirm Workflow Start</h3>
<table>
<tbody><tr>
<td><asp:button id="btnConfirmRequest" onclick="btnConfirmRequest_Click" runat="server" text="Confirm Request"></td>
<td><asp:button id="btnCancelRequest" onclick="btnCancelRequest_Click" runat="server" text="Cancel Request"></td>
</tr>
</tbody></table>

<asp:label id="lblError" style="COLOR: red" runat="server" visibile="false"></asp:label>

</asp:content>

I include the code to start the workflow in the click event of Confirm button, the reason being workflow can be only called in postback. So to generate a post back, I place the code in there.

Workflow can be started through the following code:

using (SPWeb DIAWeb = DIASite.OpenWeb())
{
btnConfirmRequest.Enabled = false;

SPWorkflowManager objWorkflowManager = null;
SPWorkflowAssociation objWorkflowAssociation = null;

SPList pictureLib =
DIAWeb.Lists[new Guid(Request["List"])];
SPListItem pictureItem = pictureLib.GetItemById(Convert.ToInt32(Request["ItemId"]));

Guid approvalWorkflowId = new Guid();
pictureLib.WorkflowAssociations.GetAssociationByBaseID(approvalWorkflowId);
objWorkflowManager = pictureItem.Web.Site.WorkflowManager;

DIAWeb.AllowUnsafeUpdates = true;
objWorkflowManager.StartWorkflow(photoItem, objWorkflowAssociation, "");
DIAWeb.AllowUnsafeUpdates = false;
}


I execute the entire code to start the workflow in SPSecurity.RunWithElevatedPrivileges because this workflow can be started for those users too which have readonly access to my picture library. But this does not resolve my problem, still I was getting error as
“Attempted to perform an unauthorized operation”

This error I do not get when accessing the site as a system account or a contributor to this library. So how do I find a workaround this problem. To resolve this, I took help of the SharePointPermissionAttribute class, it has two members Unrestricted and Impersonate, am setting both to true on my class

[SharePointPermissionAttribute(System.Security.Permissions.SecurityAction.Demand, Unrestricted=true)]

[SharePointPermissionAttribute(System.Security.Permissions.SecurityAction.Demand, Impersonate=true)]

But guess what, I still get the same error. I read somewhere that the RunWithElevatedPrivileges is helpful when all the SP objects are declared and disposed within it. Also the site object should not be created using the current httpcontext object. So I made these few changes, and my complete code listing is as follows:

using System;
using System.Configuration;
using System.Web;
using System.Web.Security;

using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Workflow;

namespace CustomPictureLibrary
{
[SharePointPermissionAttribute(System.Security.Permissions.SecurityAction.Demand, Unrestricted=true)]
[SharePointPermissionAttribute(System.Security.Permissions.SecurityAction.Demand, Impersonate=true)]

public partial class WorkflowStart : System.Web.UI.Page
{
protected Label lblError;

protected override void OnPreInit(EventArgs e)
{
base.OnPreInit(e);
string mURL = SPControl.GetContextWeb(Context).MasterUrl;
this.MasterPageFile = mURL;
}

protected void Page_Load(object sender, EventArgs e)
{
lblError.Visible = false;
}

protected void btnConfirmRequest_Click(object sender, EventArgs e)
{
try
{
//Dynamically start the workflow
SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPSite DIASite = new SPSite(this.Page.Request.Url.ToString()))
{
using (SPWeb DIAWeb = DIASite.OpenWeb())
{
btnConfirmRequest.Enabled = false;

SPWorkflowManager objWorkflowManager = null;
SPWorkflowAssociation objWorkflowAssociation = null;

SPList pictureLib =
DIAWeb.Lists[new Guid(Request["List"])];
SPListItem pictureItem = pictureLib.GetItemById(Convert.ToInt32(Request["ItemId"]));

Guid approvalWorkflowId =
new Guid();
objWorkflowAssociation = pictureLib.WorkflowAssociations.GetAssociationByBaseID(approvalWorkflowId);
objWorkflowManager = pictureItem.Web.Site.WorkflowManager;

DIAWeb.AllowUnsafeUpdates = true;
objWorkflowManager.StartWorkflow(photoItem, objWorkflowAssociation, "");
DIAWeb.AllowUnsafeUpdates = false;

SPUtility.Redirect(pictureLib.RootFolder.ServerRelativeUrl, SPRedirectFlags.UseSource, HttpContext.Current);

}
}

});
}
catch (Exception ex)
{
lblError.Text = ex.Message;
lblError.Visible = true;
}
}
}
}

Finally strong name the dll, compile it and place it in GAC. Also place the aspx form in the layouts folder, do an IIS reset and you are done!!

Hope it helps somebody and not take more days to workaround the errors I was receiving.