Thursday, January 17, 2008

To copy a file from one lib to another...

Finally back to SharePoint coding after a long hiatus of process documentation - feels great!

So the challenge has been adding a menu item to the Edit Control Block to copy items from one library to another. There are some good examples out there, Andrew Connell's blog has a great example, so I won't repeat it - but I will show all of the code I have. Might help you out.

In this scenario, I have three "document center" on my portal: Working, Staging, and Production ("Docs"). I want to let my users choose when to promote a document, but I didn't want this to be workflow dependent: ie, not based on setting a document property, or based on a check in event, etc - let the user decide.

The drop down for the Edit Control Block was the answer. "Promote Document" will now appear for users with the right access. This feature passes the list ID and item ID to an ASPX page, which then does the work of copying the document over.

Some helpful hints:

In your feature's XML file, you can assign the "Rights" value to the minimum rights a user needs in order to see your new ECB item. That way it won't show up for everyone - nice!

Inside the code, after moving the file, the field items won't copy also. So you'll need to copy them with code. But then when you perform a ListItem.Update the ModifiedBy turns in to the System Account (assuming impersonation) - ick! So intead use the ever cool SPListItem.SystemUpdate() which does *not* change the Modified or ModifiedBy properties.

Here we are:

Feature.xml
<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="1a8e5f42-5742-4c42-ad42-a31d87530042" // enter your own guid
Title="Promote Document"
Description="Promote a document from work to staging, or from staging to production" Version="1.0.0.0"
Scope="Web"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="PromoteDocument.xml" /> <
/ElementManifests>
</Feature>

The CopytoWorking.XML file, the manifest listed in feature.xml:
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<!-- Per Item Dropdown (ECB)-->
<CustomAction
Id="CopytoWorking"
RegistrationType="ContentType"
RegistrationId="0x01"
Location="EditControlBlock"
Rights="EditListItems"
Title="Promote Document">
<UrlAction Url="~site/_layouts/PromoteDocument.aspx?List={ListId}&ID={ItemId}"/> </CustomAction>
</Elements>

and finally the ASPX page. This uses the Application.Master page layout so the new page looks sort of like a SharePoint page. Worked for me!
<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Page language="C#" MasterPageFile="~/_layouts/application.master" %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ 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" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="wssawc" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="~/_controltemplates/ButtonSection.ascx" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="System.Net" %>
<asp:Content ID="Content1" ContentPlaceHolderId="PlaceHolderPageTitle" runat="server">
<asp:Literal id="PageTitleLabel1" runat="server" ></asp:Literal>
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderId="PlaceHolderPageTitleInTitleArea" runat="server">
<asp:Label id="PageTitleLabel" runat="server" ></asp:Label>
</asp:Content>
<asp:Content ID="Content3" ContentPlaceHolderId="PlaceHolderAdditionalPageHead" runat="server">
<script runat="server">
public void CancelClick(Object obj, EventArgs e)
{
SPWeb oWeb = SPContext.Current.Web;
SPList oList = oWeb.Lists[new Guid(Context.Request["List"])];
Response.Redirect(oList.DefaultViewUrl);
}

public void ConfirmClick(Object obj, EventArgs e)
{
SPWeb oWeb = SPContext.Current.Web;
SPUser currentUser = oWeb.CurrentUser;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
oWeb.AllowUnsafeUpdates = true;
SPList oList = oWeb.Lists[new Guid(Context.Request["List"])];
SPListItem oItem = oList.GetItemById(int.Parse(Context.Request["ID"]));
byte[] fileContent = oItem.File.OpenBinary();
String currentUrl = oItem.File.ServerRelativeUrl;
string[] currentUrlSplit = currentUrl.Split('/');
if (currentUrlSplit[1].CompareTo("Staging") == 0)
{ currentUrlSplit[1] = "Docs"; }
if (currentUrlSplit[1].CompareTo("Working") == 0)
{ currentUrlSplit[1] = "Staging"; }

StringBuilder sb = new StringBuilder();
String siteUrl = oWeb.ParentWeb.Url;
string[] siteUrlSplit = siteUrl.Split('/');
sb.Append(siteUrlSplit[0]); // this is the protocol
sb.Append("//");
sb.Append(siteUrlSplit[2]); // this is the server
string server = sb.ToString();
// now add the rest of the URL - site & folders
for (int i = 1; i < currentUrlSplit.Length; i++)
{
sb.Append("/");
sb.Append(currentUrlSplit[i]);
}
String newUrl = sb.ToString();
SPWeb newWeb = new SPSite(newUrl).OpenWeb();
newWeb.AllowUnsafeUpdates = true;
EnsureParentFolder(newWeb, newUrl);

// copy the file over
SPFile newFile = newWeb.Files.Add(newUrl, fileContent, currentUser, currentUser, oItem.File.TimeCreated, oItem.File.TimeLastModified);

// add the properties
foreach (SPField oField in oItem.Fields)
{
if (!oField.ReadOnlyField &&
oField.InternalName != "ContentType" &&
oField.Type != SPFieldType.Invalid &&
oField.Type != SPFieldType.WorkflowStatus &&
oField.Type != SPFieldType.File &&
oField.Type != SPFieldType.Computed &&
oField.Type != SPFieldType.User &&
oField.Type != SPFieldType.Lookup)
{
newFile.Item[oField.Title] = oItem[oField.Title];
}
}

newFile.Item.SystemUpdate(true);
newWeb.Dispose();
Response.Redirect(oList.DefaultViewUrl);
});
}

public static string EnsureParentFolder(SPWeb parentSite, string destinUrl)
{
destinUrl = parentSite.GetFile(destinUrl).Url;
int index = destinUrl.LastIndexOf("/");
string parentFolderUrl = string.Empty;
if (index > -1)
{
parentFolderUrl = destinUrl.Substring(0, index);
SPFolder parentFolder = parentSite.GetFolder(parentFolderUrl);
if (!parentFolder.Exists)
{
SPFolder currentFolder = parentSite.RootFolder;
foreach (string folder in parentFolderUrl.Split('/'))
{
currentFolder = currentFolder.SubFolders.Add(folder);
}
}
}

return parentFolderUrl;
}
</script>
</asp:Content>
<asp:Content ID="Content4" ContentPlaceHolderId="PlaceHolderPageImage" runat="server"> <img src="/_layouts/images/blank.gif" width=1 height=1 alt="" /></asp:Content>
<asp:Content ID="Content5" ContentPlaceHolderId="PlaceHolderPageDescription" runat="server">
<asp:Label id="PageDescriptionLabel" runat="server" ></asp:Label>
<asp:PlaceHolder id="PublishDescription" Visible="true" runat="server">
<tr> <TD height="8px"><img src="/_layouts/images/blank.gif" width="1" height="8" alt=""></td></tr>
<tr><td colspan="2">
<table cellpadding="2" cellspacing="1" width="100%" class="ms-informationbar" style="margin-bottom: 5px;" border=0>
<tr><td width="10" valign="middle" style="padding: 4px">
<img id="Img1" src="/_layouts/images/exclaim.gif" alt="<%$Resources:wss,exclaim_icon%>" runat="server"/></td>
<td class="ms-descriptiontext"><SharePoint:EncodedLiteral ID="EncodedLiteral1" runat="server" text="Click Confirm to continue or Cancel to return. Promoting a document will remove it from this library and move it from Work to Staging, or Staging to Production." EncodeMethod='HtmlEncode'/>
</td></tr></table>
</td></tr>
</asp:PlaceHolder></asp:Content>
<asp:Content ID="Content6" ContentPlaceholderID="PlaceHolderMain" runat="server">
<asp:Button CssClass="ms-ButtonHeightWidth" AccessKey="o" ID="Confirm" runat="server" Text="Confirm" OnClick="ConfirmClick"/>
<asp:Button CssClass="ms-ButtonHeightWidth" AccessKey="a" ID="Cancel" runat="server" Text = "Cancel" OnClick = "CancelClick" />
</asp:Content>

1 comment:

Anonymous said...

Hi Steve,

good post. Unfortunatly I´m always getting an unknown error when loading the .aspx. You have an idea?

Thanx
Stefanie