CTICoder

A random spillage of programming (and other) thoughts

Anonymous Workflow Execution in Sharepoint 2007

Posted by Shaun McDonnell on December 24, 2008

A colleague of mine had a simple task:

Create an InfoPath form that will be uploaded to Sharepoint 2007 and then allow anonymous internet users to submit that form.  Upon submission, send the respective anonymous user an email detailing out the form they filled out.

Not so simple.

Well, that’s not entirely true.  Allowing an anonymous user to submit is easy.  It was the automated Sharepoint Workflow that executed after the form was submitted that was a problem.  Why?  Workflows, by default and by design, cannot be executed anonymously.

Enter .NET, C# and me.

The requirement:

Add some custom code to Sharepoint 2007 that will allow the anonymous execution of Sharepoint Workflows.  Furthermore, create a WebPart that will give an administrative user the ability to assign anonymous workflow execution to a Sharepoint List.

Begin interesting learning process.

If you create a workflow in Sharepoint 2007 and then assign the workflow to be executed upon the ‘ItemAdded’ event of the Document Library the workflow will not fire.  What is even better is that you will get no immediate error from Sharepoint.  You’ll just come to the realization that your workflow never executed.  But never fear!  The Microsoft Windows Event Log is here!  Here’s a snapshot of what the event log will give you:

image

Yes, do not adjust your monitor.  The text actually reads “Attempted to read or write protected memory.  This is often an indication that other memory is corrupt.”

Before you decide that your server needs new memory because a stick has gone bad, realize that what this means is a process tried to execute a workflow but that process did not have the rights to do so.  Duh.

What has to be done at this point is the creation of a custom Sharepoint EventReceiver.  An EventReceiver can be assigned to any type of Sharepoint List and when the appropriate event is executed the EventReceiver kicks in.  To start this process of developing a custom EventReceiver, you create a class (i.e. ‘WorkflowReceiver’) that inherits from Microsoft.Sharepoint.SPItemEventReceiver:

public class WorkflowReceiver : SPItemEventReceiver

When inheriting from this class there are a number of virtual methods you can override and take advantage of.  For this article, we’re just going to look at the ItemAdded method.  This method executes after a new document has been added to the document library that we will eventually assign this custom EventReceiver to.  Here is an example:

public override void ItemAdded(SPItemEventProperties properties)
{
   base.ItemAdded(properties);
   StartWorkflowAnon(properties);
}

The reason I call the respective base method is so that any additional custom processing that might be assigned to this document library can continue before I do my custom work.  This is not required but recommended for lifecycle maintainability.

As the code shows, I then call a method named ‘StartWorkflowAnon’.  This is a custom method that looks like this:

void StartWorkflowAnon(SPItemEventProperties properties)
{
    SPSecurity.RunWithElevatedPrivileges(delegate()                         {
          using (SPSite site = new SPSite(properties.WebUrl))
          using (SPWeb web = site.OpenWeb())
          {
              web.AllowUnsafeUpdates = true;

              SPList list = web.Lists[properties.ListId];
              Guid wfId = new Guid(web.Properties[list.ID.ToString() + ";" + properties.EventType.ToString()]);
              SPWorkflowAssociation assoc = list.WorkflowAssociations[wfId];

              site.WorkflowManager.StartWorkflow(properties.ListItem, assoc, assoc.AssociationData);
              web.AllowUnsafeUpdates = false;
          }
    });
}

The critical code here is SPSecurity.RunWithElevatedPrivileges as this allows whatever code executing within its delegate to have the same execution rights as an authenticated user – exactly what we need to execute a workflow as an anonymous user.

A few items that we gather from the SPItemEventProperties help us determine what Sharepoint List executed the event, it’s url, the event executed and all workflows associated with the list.  Notice that when creating the wfId (workflowId) Guid we are pulling some custom properties from the current Sharepoint Website.  These properties are created by the WebPart that we’ll look into next.  Using the wfId we are able to pull the Sharepoint Workflow that we want to execute from the current Sharepoint List.  Then, we execute that respective workflow with elevated rights. 

Boom!  We now have anonymous workflow execution.  But how did it get assigned to the ItemAdded event in this list?

The Administrative WebPart

I found this to be the most difficult task to complete because creating just a basic WebPart is not easy within Sharepoint.  All html must be hardcoded and rendered via C# which can be a timely task.  Nevertheless, it has to be done.

Custom WebParts can be created using the Microsoft.SharePoint.WebPartPages.WebPart as a base object for your custom WebPart class.  Let’s call this WebPart ‘ReceiverManagement’ and thus our class definition would look like this:

public class ReceiverManagementWebPart : Microsoft.SharePoint.WebPartPages.WebPart

For an administrative user to have the ability to assign anonymous workflow execution to the event of a Sharepoint List as well as the ability to remove workflow execution from lists we are going to need to the following controls in the custom WebPart:

  • DropDownList of Sharepoint Lists
  • DropDownList of Sharepoint List Events
  • DropDownList of Sharepoint Workflows
  • Add Button
  • DataGrid of current assignments

You need to override the ‘CreateChildControls’ event of the base class to create the UI.  Here is what I have:

protected DropDownList lstSiteLists = null;
protected DropDownList lstWorkflows = null;
protected DropDownList lstListEvents = null;
protected Button btnAddReceiver = null;
protected Label lblEventName = null;
protected Label lblWorkflowName = null;
protected DataGrid gvCurrentReceivers = null;
protected Table tblReceiverManagement = null;protected override void CreateChildControls()
{
    base.CreateChildControls();

    lstSiteLists = new DropDownList();
    lstSiteLists.ID = Strings.SiteListsControlID;
    lstSiteLists.AutoPostBack = true;
    lstSiteLists.DataTextField = Strings.SiteListsTitle;
    lstSiteLists.DataValueField = Strings.SiteListsID;
    lstSiteLists.DataSource = CurrentWeb.Lists;
    lstSiteLists.SelectedIndexChanged += new EventHandler(lstSiteLists_SelectedIndexChanged);

    lstListEvents = new DropDownList();
    lstListEvents.ID = Strings.ListEventControlID;

    lstWorkflows = new DropDownList();
    lstWorkflows.ID = Strings.WorkflowsControlID;
    lstWorkflows.DataTextField = Strings.WorkflowsListDataTextField;
    lstWorkflows.DataValueField = Strings.WorkflowsListDataValueField;

    gvCurrentReceivers = new DataGrid();
    gvCurrentReceivers.ID = Strings.CurrentReceiversControlID;
    gvCurrentReceivers.CellPadding = 1;
    gvCurrentReceivers.CellSpacing = 0;
    gvCurrentReceivers.AutoGenerateColumns = false;
    gvCurrentReceivers.DeleteCommand += new DataGridCommandEventHandler(gvCurrentReceivers_DeleteCommand);
    
    ButtonColumn btnRemoveColumn = new ButtonColumn();
    btnRemoveColumn.ButtonType = ButtonColumnType.PushButton;
    btnRemoveColumn.CommandName = Strings.DeleteCommandName;
    btnRemoveColumn.Text = Strings.DeleteCommandName;
    
    BoundColumn bcListId = new BoundColumn();
    bcListId.DataField = Strings.CurrentReceiversListIdDataField;
    bcListId.Visible = false;
    
    BoundColumn bcWorkflowId = new BoundColumn();
    bcWorkflowId.DataField = Strings.CurrentReceiversWorkflowIdDataField;
    bcWorkflowId.Visible = false;

    BoundColumn bcReceiverId = new BoundColumn();
    bcReceiverId.DataField = Strings.CurrentReceiversReceiverIdDataField;
    bcReceiverId.Visible = false;
    
    BoundColumn bcListName = new BoundColumn();
    bcListName.DataField = Strings.CurrentReceiversListTitleDataField;
    bcListName.HeaderText = Strings.CurrentReceiversListTitleHeaderText;
    bcListName.HeaderStyle.HorizontalAlign = HorizontalAlign.Center;
    bcListName.HeaderStyle.VerticalAlign = VerticalAlign.Middle;
    bcListName.HeaderStyle.Font.Bold = true;
    bcListName.ItemStyle.HorizontalAlign = HorizontalAlign.Center;
    bcListName.ItemStyle.VerticalAlign = VerticalAlign.Middle;
    
    BoundColumn bcReceiverType = new BoundColumn();
    bcReceiverType.DataField = Strings.CurrentReceiversEventTypeDataField;
    bcReceiverType.HeaderText = Strings.CurrentReceiversEventTypeHeaderText;
    bcReceiverType.HeaderStyle.HorizontalAlign = HorizontalAlign.Center;
    bcReceiverType.HeaderStyle.Font.Bold = true;
    bcReceiverType.HeaderStyle.VerticalAlign = VerticalAlign.Middle;
    bcReceiverType.ItemStyle.HorizontalAlign = HorizontalAlign.Center;
    bcReceiverType.ItemStyle.VerticalAlign = VerticalAlign.Middle;

    BoundColumn bcWorkflowName = new BoundColumn();
    bcWorkflowName.DataField = Strings.CurrentReceiversWorkflowNameDataField;
    bcWorkflowName.HeaderText = Strings.CurrentReceiversWorkflowNameHeaderText;
    bcWorkflowName.HeaderStyle.HorizontalAlign = HorizontalAlign.Center;
    bcWorkflowName.HeaderStyle.VerticalAlign = VerticalAlign.Middle;
    bcWorkflowName.HeaderStyle.Font.Bold = true;
    bcWorkflowName.ItemStyle.HorizontalAlign = HorizontalAlign.Center;
    bcWorkflowName.ItemStyle.VerticalAlign = VerticalAlign.Middle;

    gvCurrentReceivers.Columns.Add(btnRemoveColumn);
    gvCurrentReceivers.Columns.Add(bcListId);
    gvCurrentReceivers.Columns.Add(bcWorkflowId);
    gvCurrentReceivers.Columns.Add(bcReceiverId);
    gvCurrentReceivers.Columns.Add(bcListName);
    gvCurrentReceivers.Columns.Add(bcReceiverType);
    gvCurrentReceivers.Columns.Add(bcWorkflowName);

    tblReceiverManagement = new Table();
    tblReceiverManagement.ID = Strings.ReceiverManagementTableControlID;
    tblReceiverManagement.CellPadding = 0;
    tblReceiverManagement.CellSpacing = 0;
    tblReceiverManagement.BorderWidth = new Unit(0);

    TableCell tdListLabel = new TableCell();
    tdListLabel.Text = Strings.ListsLabelText;

    TableCell tdLists = new TableCell();
    tdLists.Controls.Add(lstSiteLists);

    TableCell tdEventsLabel = new TableCell();
    tdEventsLabel.Text = Strings.EventsLabelText;

    TableCell tdEvents = new TableCell();
    tdEvents.Controls.Add(lstListEvents);

    TableCell tdWorkflowsLabel = new TableCell();
    tdWorkflowsLabel.Text = Strings.WorkflowsLabelText;

    TableCell tdWorkflows = new TableCell();
    tdWorkflows.Controls.Add(lstWorkflows);

    TableCell tdReceivers = new TableCell();
    tdReceivers.RowSpan = 2;
    tdReceivers.Controls.Add(gvCurrentReceivers);

    btnAddReceiver = new Button();
    btnAddReceiver.Text = "Add";
    btnAddReceiver.Click += new EventHandler(btnAddReceiver_Click);

    TableCell tdButton = new TableCell();
    tdButton.RowSpan = 2;
    tdButton.Controls.Add(btnAddReceiver);

    TableRow trListsRow = new TableRow();
    trListsRow.Cells.Add(tdListLabel);
    trListsRow.Cells.Add(tdLists);

    TableRow trEventsRow = new TableRow();
    trEventsRow.Cells.Add(tdEventsLabel);
    trEventsRow.Cells.Add(tdEvents);

    TableRow trWorkflowsRow = new TableRow();
    trWorkflowsRow.Cells.Add(tdWorkflowsLabel);
    trWorkflowsRow.Cells.Add(tdWorkflows);

    TableRow trButtonRow = new TableRow();
    trButtonRow.Cells.Add(tdButton);

    TableRow trReceiversRow = new TableRow();
    trReceiversRow.Cells.Add(tdReceivers);

    tblReceiverManagement.Rows.Add(trListsRow);
    tblReceiverManagement.Rows.Add(trEventsRow);
    tblReceiverManagement.Rows.Add(trWorkflowsRow);
    tblReceiverManagement.Rows.Add(trButtonRow);
    tblReceiverManagement.Rows.Add(trReceiversRow);

    base.Controls.Add(tblReceiverManagement);

    BindData();

}

 

All of this code successfully creates the UI of the WebPart.  Here is the BindData method:

protected void BindData()
{
    LogHelper.WriteDebug("BindData() called.");
    lstSiteLists.DataBind();
    BindEvents();
    BindWorkflows();
    BindCurrentSubscriptions();
}

Within the BindData method I am binding the respective lists, events, workflows and existing assignments to their controls.  You can download that code using the link at the end of this article.  For now, let’s take a look at what happens when the ‘Add’ button is clicked:

protected void btnAddReceiver_Click(object sender, EventArgs e)
{
    SPList list = CurrentWeb.Lists[new Guid(lstSiteLists.SelectedValue)];

    string key = String.Format("{0};{1}", lstSiteLists.SelectedValue, lstListEvents.SelectedValue);
    if (CurrentWeb.Properties.ContainsKey(key))
    {
        CurrentWeb.Properties.Remove(key);
    }

    CurrentWeb.Properties.Add(key, lstWorkflows.SelectedValue);
    CurrentWeb.Properties.Update();

    SPEventReceiverType receiverType = (SPEventReceiverType)Enum.Parse(typeof(SPEventReceiverType), lstListEvents.SelectedValue);
    list.EventReceivers.Add(receiverType, Assembly.GetExecutingAssembly().FullName, CustomEventReceiverClassName);
}

Is is here where we create the information needed by the WorkflowReceiver (see class above) to execute the anonymous workflow.  Let’s step through the above method.

The CurrentWeb object is a local property that just retrieves the current SPWeb object where the WebPart resides.  So, first we are retrieving the list the user selected in the DropDownList that they wish to assign the workflow to.  Then, I create a unique key that is formed by the unique Guid of the list they selected, a semicolon delimiter and then the name of the event they chose. 

I then check the custom properties of the CurrentWeb to see if that key already exists.  If so, I remove that property from the web because this is a duplicate event assignment and then I proceed to re-add the same property to the CurrentWeb.  I could have chosen to inform the user that the assignment they are creating already exists but this was easier.

Using the enumerator named SPEventReceiverType I am able to cast the text-based name of the event the user chose from the dropdown to this enumerator.  Then, I add a new receiver to the user-selected list and pass it the type of event, the FQ name of the assembly that contains our custom EventReceiver (in this case it is the same assembly where the current code is executing) and finally I pass it a constant string variable that contains the full name of the class (or type) to be executed when the event is tripped.

Our custom EventReceiver (WorkflowReceiver) is now wired up correctly and ready for production.

Deploying this code to a Sharepoint 2007 Server is a little tricky.  First, all of your assemblies need to be strongly signed and uniquely versioned.  These assemblies then need to installed into the GAC (Global Assembly Cache) on the same server from which Sharepoint 2007 is running.  Finally, you need to edit the main web.config for Sharepoint and the following SafeControl lines:

<SafeControl Assembly="Cti.SharepointServices.ReceiverManagement, Version=2.0.0.12, Culture=neutral, PublicKeyToken=e59deda35424cfa7" 
Namespace="Cti.SharepointServices.ReceiverManagement"
TypeName="*"
Safe="True" />

Adding your new assembly as SafeControl ensures that Sharepoint can trust this assembly and therefore allow for its execution.

Lastly, you need to create a dwp file so you can import your new WebPart into Sharepoint.  Your dwp file should contain something like this:

<?xml version="1.0" encoding="utf-8"?>
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2">
    <Assembly>Cti.SharepointServices.ReceiverManagement, Version=2.0.0.12, Culture=neutral, PublicKeyToken=e59deda35424cfa7</Assembly>
    <TypeName>Cti.SharepointServices.ReceiverManagement.ManageReceiversWebPart</TypeName>
    <Title>Workflow Customization</Title>
</WebPart>

You can then import this DWP file from the Sharepoint interface and then drag your WebPart onto the page.  It should look something like this:

image

Once assigned, your workflows will now execute when submitted by anonymous users.

To download the code that relates to this post, go here.

-Shaun


dp.SyntaxHighlighter.ClipboardSwf = ‘http://s1.wordpress.com/wp-content/plugins/highlight/clipboard.swf&#8217;;
dp.SyntaxHighlighter.HighlightAll(‘code’);

13 Responses to “Anonymous Workflow Execution in Sharepoint 2007”

  1. steve said

    Unfortunately, having an Event Recevier on a list that will fire when someone creates/edit etc an item will also fail when an Anonymous user creates the item.
    i get “server out of memory”.
    i have even tried to simple Cancel the insert when adding – works fine for logged in user, fails for anonymous user.

  2. Amar Dave said

    Hi shaun,
    Here’s my issue. I have a public facing website, which does not have any FBA, its just has anonymous access enabled on entire website and custom lists. Know when i associate a workflow with the custom list, i get a error while adding item as anonymous user. The solution above is for the same issue, but my question is where do i add the code, cause i have a sharpoint designer out of the box workflow and what are the instruction for implementing it. Thanks
    Amar

  3. Ruselle said

    Amar Dave

    “Deploying this code to a Sharepoint 2007 Server is a little tricky. First, all of your assemblies need to be strongly signed and uniquely versioned. These assemblies then need to installed into the GAC (Global Assembly Cache) on the same server from which Sharepoint 2007 is running. Finally, you need to edit the main web.config for Sharepoint and the following SafeControl lines:

    Adding your new assembly as SafeControl ensures that Sharepoint can trust this assembly and therefore allow for its execution.

    Lastly, you need to create a dwp file so you can import your new WebPart into Sharepoint. Your dwp file should contain something like this:

    Cti.SharepointServices.ReceiverManagement, Version=2.0.0.12, Culture=neutral, PublicKeyToken=e59deda35424cfa7
    Cti.SharepointServices.ReceiverManagement.ManageReceiversWebPart
    Workflow Customization

    You can then import this DWP file from the Sharepoint interface and then drag your WebPart onto the page.”

    I’ve tried this and it does not work. I get a “Server Out of Memory” error.

  4. Richard said

    Very impressive website, good job

  5. steve said

    Do you have this in a MS VS 2008 project or a package that can be deployed to SP Farm?

  6. majox said

    Hi I have tried to add an event receiver to list with your tutorial, but its not working . Even when I have so little code like this its not working:
    public override void ItemAdded(SPItemEventProperties properties)
    {

    base.ItemAdded(properties);

    }

  7. Anonymouse said

    The download link doesn’t seem to be working, would love to look at the complete code for your solution. Thanks!

  8. Me too! said

    Did you ever get a link? having this exact problem with anonymous workflows. would be really useful to get a copy of the solution. thanks for sharing!

  9. Sorry everyone, I cannot seem to be able to find the code I had originally uploaded for this article. However, I know that it works because I have it deployed in a production environment that I no longer have access to. I will see if I can rewrite it and upload it.

  10. Sharepoint for Dummies…

    […]Anonymous Workflow Execution in Sharepoint 2007 « CTICoder[…]…

Leave a reply to Me too! Cancel reply