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.