Tuesday, October 16, 2012

Creating Custom Workflow Activity for Attachments

First of all let me declare , this is not one of the latest thing you can do with SharePoint but I thought this might be helpful (at least to me)
If you have developed any list workflow using SharePoint designer , you probably have noticed that there is no way to work with list item attachments. to overcome such scenarios you have to create your own workflow activity and deploy it on server. in Office365 case , you have Sandbox solution capabilities using which you can achieve the same.

In this case , I have tried to create a custom workflow activity which reads all attachments of the list item on which workflow is running and saves them to mentioned document library. while saving , it creates a folder whose name is a combination of list item's Id and Title and then saves attachments into it. I have create this as a Sandbox solution so that anyone can easily deploy this wsp package on their Office365 Site and start using it.



and custom action can be used like



To create your custom workflow actions basically three things are needed.
  • Activity Action Class - This is the class where you will write your custom logic which will be executed when activity occurs in workflow.
  • Elements.xml - You will need to use <WorkflowActions> Element in order to deploy your custom action to SharePoint.
  • Feature - finally , a Site Collection scoped feature which will contain elements.xml you created to deploy your action to SharePoint.


So lets start creating a custom workflow activity for above mentioned case.

I have created a simple SharePoint 2010 empty solution using Visual Studio 2010 SharePoint project template.

Now lets add the Activity action class. Note that SaveItemAttachments method will be executed whenever our custom activity will be called from SPD workflow. this method takes a string method argument which is passed from SPD , as a document library name.



public class AttachmentActions
{
   public List<SPFile> filesCollection = null;
   public string tdocLib = string.Empty;
   SPUserCodeWorkflowContext tempcontext = null;

   public Hashtable SaveItemAttachments(SPUserCodeWorkflowContext context, string targetDocumentLibrary)
   {
     Hashtable results = new Hashtable();
     results["Exception"] = string.Empty;
     tdocLib = targetDocumentLibrary;
     tempcontext = context;
     try
     {
       //Get SiteCollection
       using (SPSite site = new SPSite(context.CurrentWebUrl))
       {
         //Get Web
         using (SPWeb web = site.OpenWeb())
         {
           //Get List
           SPList list = web.Lists.GetList(context.ListId, false);
           if (list != null)
           {
            //Access List Item
            SPListItem listItem = list.GetItemById(context.ItemId);
            if (listItem != null)
            {
             //Check for attachments
             if (listItem.Attachments != null && listItem.Attachments.Count > 0)
             {
              //Get All Attachments
              filesCollection = InitializeFilesCollection(web, listItem);
              if (filesCollection != null)
              {
                //Get Target Document Library
                SPList targetLibrary = web.GetList(targetDocumentLibrary);
                if (targetLibrary != null)
                {
                  //Upload attachment to document library
                  foreach (SPFile attachmentFile in filesCollection)
                  {
                    byte[] fileContents = GetFileContents(attachmentFile);
                    if (fileContents != null)
                    {
                      string folderName = string.Format("{0}.{1}", listItem.ID, listItem.Title);
                      string attachmentName = attachmentFile.Name;
                      //Create Folder in document library
                      SPFolder foundFolder = CreateFolder(folderName, targetLibrary, targetLibrary.RootFolder.SubFolders);
                      if (foundFolder != null)
                      {
                        //Add file to created folder
                        SPFile addedFile = foundFolder.Files.Add(string.Format("{0}/{1}", foundFolder.Url, attachmentName), fileContents, true);
                        foundFolder.Update();
                        }
                      }
                    }
                    targetLibrary.Update();
                    results["Status"] = "Success";
                 }
                }
               }
              }
            }
           }
         }
        }
         catch (Exception ex)
        {
          results["Exception"] = ex.ToString();
        }

        return (results);
       }

        public List<SPFile> InitializeFilesCollection(SPWeb web, SPListItem listItem)
        {
          SPAttachmentCollection allAttachments = listItem.Attachments;
          string attachmentUrlPrefix = allAttachments.UrlPrefix;

          if (allAttachments != null && allAttachments.Count > 0)
          {
            filesCollection = new List<SPFile>();
            foreach (string attachmentName in allAttachments)
            {
              string attachmentPath = string.Format("{0}{1}", attachmentUrlPrefix, attachmentName);
              SPFile attachmentFile = web.GetFile(attachmentPath);
              if (attachmentFile != null)
              {
                 filesCollection.Add(attachmentFile);
              }
            }
          }

            return filesCollection;
        }

        public SPFolder CreateFolder(string folderName, SPList docLib, SPFolderCollection subFolders)
        {
          SPFolder foundFolder = null;
          if (subFolders.Count > 0)
          {
            foreach (SPFolder folder in subFolders)
            {
              if (folder != null && !string.IsNullOrEmpty(folder.Name))
              {
                if (folder.Name.Equals(folderName))
                {
                  foundFolder = folder;
                    break;
                  }
                }
              }
           //If no matching folder found - create new folder in document library root folder.
              if (foundFolder == null)
              {
                if (!docLib.EnableFolderCreation)
                {
                  docLib.EnableFolderCreation = true;
                  docLib.Update();
                }
                foundFolder = subFolders.Add("/" + tdocLib + "/" + folderName);
                docLib.Update();
              }
            }
            return foundFolder;
        }

        public byte[] GetFileContents(SPFile file)
        {
            return file.OpenBinary();
        }

    }



Now after this , let SharePoint know that what to execute whenever action will be called. This can be done by writing a elements file. SharePoint gives you a way to deploy such actions using <WorkflowActions> element where you can specify number of parameters.

Add empty element to the solution and add following xml in it.



<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <WorkflowActions>

    <!--Save Attachments Action-->
    <Action Name="Move Attachments to library"
        SandboxedFunction="true"
        Assembly="$SharePoint.Project.AssemblyFullName$"
        ClassName="SharePoint.Sandbox.WorkflowActions.ActivityClasses.AttachmentActions"
        FunctionName="SaveItemAttachments"
        AppliesTo="all"
        UsesCurrentItem="true"
        Category="Custom Actions">
      <RuleDesigner Sentence="Copy Attachments to library %1">
        <FieldBind Id="1" Field="targetDocumentLibrary" DesignerType="Text" Text="library name"/>
      </RuleDesigner>
      <Parameters>
        <Parameter Name="__Context" Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext, Microsoft.SharePoint.WorkflowActions" Direction="In" DesignerType="Hide" />
        <Parameter Name="targetDocumentLibrary" Type="System.String, mscorlib" Direction="In" DesignerType="TextBox" />      
      </Parameters>
    </Action>
   
  </WorkflowActions>
</Elements>


If you observe the <RuleDesigner> tag , there you can specify the actual sentence which will be seen in the SharePoint designer after adding this action. %1 is the input parameter for taking the document library name from the end user. this input is provided to the custom activity executing method.

As you added empty element in the solution , visual studio by default adds a feature to hold this elements.xml. Make sure that the scope of the feature is Site. 

After this you are ready to deploy this solution to your environment.

After deployment , you can create workflow using this activity and check the result.

Above example shows , after workflow completion attachments were saved to document library.



You can download the source code and wsp here.


6 comments:

  1. Looks good. I have on question, which is probably stupid, how do you determine the "Library name" field? Should we type the GUID, the relative path, the relative URL, etc?

    ReplyDelete
  2. @Yuri: Example above takes library name as input argument in workflow designer. In this particular example I have simply typed and passed the name of the document library in SPD.

    ReplyDelete
  3. Hi Bhushan, this is great - thanks for sharing it! I have deployed the WSP file to a test SharePoint 2010 Enterprise Server on our network. I created a list (announcements) and a regular document library. I created a very simple workflow, just using the 'move attachments to library' action - then published to the list.

    However, when I add a new item with an attachment (with very simple, one word, name) the workflow returns an error:

    {ec9fa592-cabd-4a04-880c-b6e1f50096e4} AttachmentCopy {91d53a40-4844-4365-b628-1c28677df5c9} AttachmentCopyTest Announcement3 System Account 2013-04-11T15:04:49 Error 0 0.000797725 An error has occurred in AttachmentCopy.

    I can't see anything in the SP logs or in the application event logs. I wonder if you might have any ideas.

    Many thanks

    ReplyDelete
  4. Hi Bhushan, I have the same error as above.....any ideas please? This is brilliant and also a little urgent! tHanks

    ReplyDelete
  5. I am getting an error as well, but it only seems to be when on a sub-site. It runs fine on the rootweb of the Site Collection. I think I ran through about every possible way to try and get the sub-site object in the code with no luck using the various properties available in the SPUserCodeWorkflowContext object.

    In theory (per this post and supporting information: http://www.dotnetmafia.com/blogs/dotnettipoftheday/archive/2008/02/14/how-to-get-a-spweb-object-given-a-full-url.aspx), the SPSite object should open the SPWeb at the lowest level passed in to the SPSite constructor when no arguments are passed to the OpenWeb method, regardless of level in the hierarchy, but either that is failing or something below that line of code is not working with the current web object. I am having trouble debugging this (not as familiar with Sandbox solutions and their restrictions).

    I am pretty much stuck and will use it at the rootweb level of the Site Collection to meet my needs; however, maybe this comment will give someone a hint to persue and correct the issue.

    ReplyDelete
  6. Hi Guys, apologies for late response.
    Above solution was developed and tested with considering root web as current web.
    You may want to do few changes in order to make this as generic.
    However , Did you try to debug the activity? You can add few Log statements in the code and see those in workflow history. This will give you an idea that up to which point activity is doing fine.
    Try using SPWorkflow.CreateHistoryEvent method to log your comments in the workflow.

    ReplyDelete