Wednesday, April 10, 2013

Custom workflow action to Move a file

I recently worked on a project where users wanted an automated process to archive files, i.e. move them from a working location to a secured document center where they are retained for a specified amount of time. The expectation was that an end user would select a file, click a button and the file magically moved to a specified location.
So after a few weeks of rigorous googling, I came up with this solution. This solution has two parts to it.
1. Create a custom workflow activity to move a file from one location to another (does not span site collections though)

2. Modify the ribbon on the list/library settings panel to include a custom button which performs a custom activity.

Few notes on the activity:
1. This activity is used to move file from current library to another library within a site collection.
2. This activity does not span across site collection.
3. This activity preserves the modified/created dates and author values.
4. This activity copies all associated metadata with a file as long as the same fields are created in the destination library.
5. This activity does not preserve the version history of a file.
6. This activity checks to see if a file with the same name already exists on the destination and if it does, an error message to the workflow history is written.
7. This activity checks out the source file before moving it to the destination. If the move activity is unsuccessful, it will write an error message to the workflow history and check in the file with a comment.
8. The result of the move activity is written out to a Boolean variable in the form of true or false.
9.  MoveTo() or CopyTo() functions cannot be used to span across sites. Hence Add() and Delete() methods were used in this code.

Create a custom workflow activity using Visual Studio 2010

1. Create a new empty SharePoint Project and give it an appropriate name, Eg: WorkflowActions
2. Choose to deploy it as a farm solution
3. Now, right click on the solution name in the solution explorer and click new project

4. Create a workflow activity library and give it an appropriate name eg: MoveFileAction
 
5. Right Click on the MoveFileAction project and add references to
Microsoft.SharePoint
Microsoft.SharePoint.WorkflowActions
6. Rename the Activity1.cs file that is automatically created under MoveFileAction Project to MoveFile.cs

7. Add the following code to the class file MoveFile.cs
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.WorkflowActions;

namespace MoveFileAction
{
    // This activity is used to move file from one library to another within a site collection.
    // This activity preserves the modified/created dates and author values
    // This activity copies all associated metadata with a file.
    // This activity does not span across site collections.

    public partial class MoveFile : SequenceActivity
    {
        public MoveFile()
        {
            InitializeComponent();
        }
        //Destination Site URL
        public static DependencyProperty SiteUrlProperty = DependencyProperty.Register("SiteUrl",
         typeof(string), typeof(MoveFile), new PropertyMetadata(""));
        [DescriptionAttribute("Url of site where item is to be moved to")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]
        public string SiteUrl
        {
            get { return ((string)(base.GetValue(MoveFile.SiteUrlProperty))); }
            set { base.SetValue(MoveFile.SiteUrlProperty, value); }
        }

        //Destination Library Name
        public static DependencyProperty LibraryNameProperty =
          DependencyProperty.Register("LibraryName", typeof(string),
          typeof(MoveFile), new PropertyMetadata(""));
        [DescriptionAttribute("Name of library to move item to")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]
        public string LibraryName
        {
            get { return ((string)(base.GetValue(MoveFile.LibraryNameProperty))); }
            set { base.SetValue(MoveFile.LibraryNameProperty, value); }
        }

        //Current Workflow Context
        public static DependencyProperty __ContextProperty =
          System.Workflow.ComponentModel.DependencyProperty.Register(
          "__Context", typeof(WorkflowContext), typeof(MoveFile));
        [Description("Context")]
        [Category("Context")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public WorkflowContext __Context
        {
            get { return ((WorkflowContext)(base.GetValue(MoveFile.__ContextProperty))); }
            set { base.SetValue(MoveFile.__ContextProperty, value); }
        }

        // Current List ID
        public static DependencyProperty __ListIdProperty =
          System.Workflow.ComponentModel.DependencyProperty.Register("__ListId", typeof(string), typeof(MoveFile));
        [Description("List Id")]
        [Category("List Id")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string __ListId
        {
            get { return ((string)(base.GetValue(MoveFile.__ListIdProperty))); }
            set { base.SetValue(MoveFile.__ListIdProperty, value); }
        }

        // Current List Item
        public static DependencyProperty __ListItemProperty =
          System.Workflow.ComponentModel.DependencyProperty.Register(
          "__ListItem", typeof(int), typeof(MoveFile));
        [Description("List Item")]
        [Category("List Item")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int __ListItem
        {
            get { return ((int)(base.GetValue(MoveFile.__ListItemProperty))); }
            set { base.SetValue(MoveFile.__ListItemProperty, value); }
        }

        //Output Variable
        public static DependencyProperty __OutputVariableProperty =
         System.Workflow.ComponentModel.DependencyProperty.Register(
         "__OutputVariable", typeof(bool), typeof(MoveFile));
        [Description("Output Variable")]
        [Category("Output Variable")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public bool __OutputVariable
        {
            get { return ((bool)(base.GetValue(MoveFile.__OutputVariableProperty))); }
            set { base.SetValue(MoveFile.__OutputVariableProperty, value); }
        }

        public Guid workflowId = default(System.Guid);
        public SPWorkflowActivationProperties workflowProperties = new SPWorkflowActivationProperties();

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {

             //Get the current context items.
            SPWeb contextWeb = __Context.Web;
            SPList contextList = contextWeb.Lists[new Guid(__ListId)];
            SPListItem contextItem = contextList.GetItemById(__ListItem);
            SPFile contextFile = contextItem.File;

            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                // Get the target Site collection
                using (SPSite targetSite = new SPSite(SiteUrl))
                {
                    //Get the target site
                    using (SPWeb targetWeb = targetSite.OpenWeb())
                    {
                        try
                        {
                            if (contextFile != null)
                            {
                                //Construct the destination File Path
                     SPFolder targetFolder = targetWeb.RootFolder.SubFolders[LibraryName];
                     string strfutureFilePath = targetFolder + "/" + contextFile.Name;

                                //Check out the source file                          
                                SPWorkflow.CreateHistoryEvent(targetWeb, __Context.WorkflowInstanceId, 0, targetWeb.CurrentUser, TimeSpan.Zero, "Information", "Checking out source file", string.Empty);
                                if (contextFile.CheckOutType == SPFile.SPCheckOutType.None)
                                {
                                    contextFile.CheckOut();
                                }
                                else //If already checked out, undo previous checkout and checkout again.
                                {
                                    contextFile.UndoCheckOut();
                                    contextFile.CheckOut();
                                }

                                //First test to see if the file exists on the target
                                SPFile isItThere = targetWeb.GetFile(strfutureFilePath);
                                if (isItThere.Exists)
                                {
                                    // Log message to workflow history
                                    SPWorkflow.CreateHistoryEvent(targetWeb, __Context.WorkflowInstanceId, 0, targetWeb.CurrentUser, TimeSpan.Zero, "Information", "Target file exists", string.Empty);

                                    // Set the output variable to false which indicates the move was not successful
                                    this.__OutputVariable = false;

                                    //Check in the file with comment.
                                    contextFile.CheckIn("Target File exists");

                                }
                                else
                                {
                                    SPWorkflow.CreateHistoryEvent(targetWeb, __Context.WorkflowInstanceId, 0, targetWeb.CurrentUser, TimeSpan.Zero, "Information", "Moving file to destination", string.Empty);

                                    //Set Destination site-relative URL of the file
                                    SPFileCollection collFiles = targetFolder.Files;
                                    string destFileURL = collFiles.Folder.Url + "/" + contextFile.Name;

                                    //A byte Array that contains the file
                                    byte[] binFile = contextFile.OpenBinary();

                                    // Get the modified/created dates and author value from source file.
                                    SPListItemVersion version = contextItem.Versions[0];
                                    SPUser userAuthor = contextFile.Author;
                                    SPUser userModified = contextFile.ModifiedBy;
                                    DateTime dtModified = version.Created.ToLocalTime();
                          DateTime dtCreated = ((DateTime)version["Created"]).ToLocalTime();

                                    // Add the file from source to destination using the overload methd SPFileCollection.Add method (String, Byte[], SPUser, SPUser, DateTime, DateTime)
SPFile movedFile = collFiles.Add(destFileURL, binFile, userAuthor, userModified, dtCreated, dtModified);

                                    //Update the destination file properties
                                    SPListItem listItem = movedFile.Item;
                                    listItem["Created"] = dtCreated.ToLocalTime();
                                    listItem["Modified"] = dtModified.ToLocalTime();
                                    listItem["Created By"] = userAuthor;
                                    listItem["Modified By"] = userModified;
                                    listItem.Update();

                                    //Delete the source file
                                    contextFile.Delete();

                                    // Set the output variable to true which indicates the move was succesfull
                                    this.__OutputVariable = true;
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            string Message = ex.Message;
                            SPWorkflow.CreateHistoryEvent(targetWeb, __Context.WorkflowInstanceId, 0,
                              targetWeb.CurrentUser, TimeSpan.Zero, "Error",
                              "Message: " + Message, string.Empty);
                            throw ex;
                        }

                    }
                }
            });

            return ActivityExecutionStatus.Closed;
        }
    }
}

8. Add a mapped folder to the project.
Right Click on WorkflowActions and add SharePoint Mapped folder

 Select Workflow folder under Template

9. Add an XML file to this folder and rename it to WorkflowActions.actions
The definition of ".actions" files is responsible for making the custom workflow activity appear in the SharePoint Designer 2010
10. Add the following code to the .actions file. This code snippet must include mappings to the custom activity

<?xml version="1.0" encoding="utf-8" ?>
<WorkflowInfo>
  <Actions Sequential="then" Parallel="and">
    <Action Name="Move File"
            ClassName="MoveFileAction.MoveFile"
            Assembly="MoveFileAction, Version=1.0.0.0, Culture=neutral, PublicKeyToken=/*Public Key Token of your project*/"
            Category="Custom workflow Actions"
            AppliesTo="all">
      <RuleDesigner Sentence="Move File to Library %2 at Site URL %1(Output to Variable %3)">
        <FieldBind Field="SiteUrl" Text="Site URL" DesignerType="TextArea" Id="1"/>
        <FieldBind Field="LibraryName" Text="Library Name" DesignerType="TextArea" Id="2"/>
        <FieldBind Field="__OutputVariable" Text="Output" DesignerType="ParameterNames" Id="3"/>
      </RuleDesigner>
      <Parameters>
        <Parameter Name="SiteUrl" Type="System.String, mscorlib" Direction="In" DesignerType="StringBuilder"
           Description="URL of the destination site. Must be in the form of http://ServerName/(SiteName)" />
        <Parameter Name="LibraryName" Type="System.String, mscorlib" Direction="In" DesignerType="StringBuilder"
           Description="Name of the destination library." />
        <Parameter Name="__Context"
          Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext, Microsoft.SharePoint.WorkflowActions"
          Direction="In" DesignerType="Hide"/>
        <Parameter Name="__ListId" Type="System.String, mscorlib, mscorlib" Direction="In" DesignerType="Hide"/>
        <Parameter Name="__ListItem" Type="System.Int32, mscorlib, mscorlib" Direction="In" DesignerType="Hide"/>
        <Parameter Name="__OutputVariable" Type="System.Boolean, mscorlib, mscorlib" Direction="Out"/>
      </Parameters>
    </Action>
  </Actions>
</WorkflowInfo>

 11. Sign the assembly for the project
Right Click on MoveFileAction and
select properties --> Signing --> New --> Enter Key Name --> disable password option

12. Build the Solution. This will generate .dll files under bin/Debug folder of the project
Now, we need to add the assemnly for MoveFileAction in the Package

13. Under WorkflowActions project, double click on the .package file
14. Select the advance tab at the very bottom of the window

15. Add assembly to the package
 16. Modify the web.config file to add the assembly
Open IIS Manager
Expand the server name and right click on your SharePoint Site and click "Explore"

Edit the web.config file.
Under <System.Workflow.ComponentModel.WorkflowCompiler>
Add the authorized type
<authorizedType Assembly="MoveFileAction", Version=1.0.0.0, Culture=neutral, PublicKeyToken="/*Public Key token of your project*/" NameSpace="MoveFileAction" TypeName="*" Authorized="True"/>

17. Save All and Deploy solution
18. Open SharePoint Designer and create a new workflow
You can now find the custom activity under the Actions tab.

To deploy the solution on other servers, copy the .wsp file from the bin/debug folder of the main project and deploy at farm level.
//Note: Do not upload solution to Solutions gallery under site collection settings as this is a farm solution. 
Add the authorized type property to web.config file of each web server running on a environment.
//Note:If the web.config file is not updated on each web server, the activity will show up on the actions drop down list but will not add to the workflow

To find the public key token of a project

On visual Studio, go to the tools menu and choose external tools /
//Make sure you are on one of the elements of the project you want the public key token for. Each sub project will have separate public Key token
This will bring up a new dialog window.
Click the Add button and add the following

Title: [New Tool1] //can be any relevant name 
Command: C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bin\sn.exe //some versions may have the sn.exe file under folder v6.0A
Arguments: -Tp $(TargetPath)
Check the Use Output Window check box.

The final looks like this:

Then choose Tools --> [New Tool1] //name of tool, and in the output window you will see the following:

Build a workflow to move file

Add the following steps to your workflow logic


Add a custom button to List/Library ribbon
open the list/library in SharePoint designer
Click on “Custom Actions” drop down
To see the action show up as a button on ribbon, select “View Ribbon” in the drop down
Give an appropriate name, description
Choose what you want the button to do when clicked.
Attach an image to the button
Give a sequence number to determine the position of the button on the ribbon
0- first position
1- Second
Etc..


Click ok. The button now shows on the list/library when a document/list item is selected.
The action also shows on the drop down when a document/list item is selected

Thanks and hope this helps build your solution. I am not a code pro so pardon me if there are any mistakes in my code.

References:

2 comments:

  1. Hi Karu,

    I cannot be sure about it, as I am not familiar with 2013 environment yet, but I assume it will (assumption base: Methods used in this code haven't changed from 2010 to 2013. Ref:http://msdn.microsoft.com/en-us/library/ms448353.aspx). In case you try it, please let us know too.

    Thanks!

    ReplyDelete