Friday, February 22, 2008

Recommended Reading

Here's a list of recommended reading by a well known MOSS MVP. For help rationalizing poor decision making, I recommend Mistakes Were Made (But Not by Me).

Thursday, February 21, 2008

Delete SharePoint Document Library Files Programmatically

Following on from this post on moving or copying SharePoint files with FrontPage Remote Procedure Calls (RPC), and this older post on remotely saving files to a SharePoint document library, the code below demonstrates how to remove files programmatically from a remote document library without using the SharePoint API. Documentation about the move method can be found here. The EscapeVectorChars method can be found in the original post. Obviously because this RPC method will remove documents from your site (potentially deleting them permanently), please use with appropriate caution!

Update: You can download a comprehensive c# class library to automate RPC calls - including removing files. See this blog post for more information.

        public static bool Remove(string webUrl, out string result, params string[] webRelativeUrls)
        {
            string method = "method=remove+documents%3a12.0.4518.1016&service_name=%2f&url_list=[{0}]";
            method = String.Format(method, GetStringVector(webRelativeUrls));
            try
            {
                using (WebClient webClient = new WebClient())
                {
                    webClient.Credentials = CredentialCache.DefaultCredentials;
                    webClient.Headers.Add("Content-Type", "application/x-vermeer-urlencoded");
                    webClient.Headers.Add("X-Vermeer-Content-Type", "application/x-vermeer-urlencoded");
                    result = Encoding.UTF8.GetString(webClient.UploadData(webUrl + "/_vti_bin/_vti_aut/author.dll", "POST", Encoding.UTF8.GetBytes(method)));
                    if (result.IndexOf("\n<p>message=successfully") < 0) //note: will show success even if document(s) did not exist on the server
                        throw new Exception(result);
                }
            }
            catch (Exception ex)
            {
                result = ex.Message;
                return false;
            }
            return true;
        }
 
        static string GetStringVector(string[] strings)
        {
            StringBuilder sb = new StringBuilder();
            foreach (string s in strings)
            {
                sb.Append(EscapeVectorChars(s));
                sb.Append(";");
            }
            return sb.ToString().TrimEnd(';');
        }

Monday, January 28, 2008

Searching for Users or Groups Using SPUtility.ResolvePrincipal

It's sometimes handy to know that the Person or Group SharePoint column data type is stored internally as a SPFieldUserValue. This in turn is derived from SPFieldLookupValue which is serialized as "ID;#VALUE" in the content database, e.g.:

        SPListItem item = web.Lists["Docs"].Items[0];
        string fieldValue = item["Contact"].ToString(); // Contact is the name of Person or Group column and in this case has a value "1;#Test User"
        SPFieldUserValue user = new SPFieldUserValue(web, fieldValue);
        string name = user.LookupValue; // Test User


The LookupId for this field type is the ID property of the SPPrincipal class.

Sometimes you might need to determine what this Lookup ID is for a particular user name (you can even search for a user by email address), and that's where the SPUtility.ResolvePrincipal method is helpful, e.g.:

        using Microsoft.SharePoint.Utilities;
        ...
        SPPrincipalInfo userInfo = SPUtility.ResolvePrincipal(web, "Test User", SPPrincipalType.All, SPPrincipalSource.All, null, false);
        if (userInfo != null)
        {
            item["Contact"] = new SPFieldUserValue(web, userInfo.PrincipalId, userInfo.DisplayName);
            item.Update();
        }


A good example of using this method can be found here which demonstrates using this method to find out if a user has access to your site or list.

A second SPUtility method - SPUtility.SearchPrincipals - returns a list of principals much like the list returned by the "Select People and Groups" picker dialog box, e.g:

        bool reachedMaxCount = false;
        IList<SPPrincipalInfo> users = SPUtility.SearchPrincipals(web, "T", SPPrincipalType.All, SPPrincipalSource.All, null, 100, out reachedMaxCount);

Thursday, December 6, 2007

Move or Copy SharePoint Document Library Files Programmatically

In this post I published some code that took a look at using FrontPage Server Extentions to upload a file to a document library where the requirement was to be able to save metadata along with the document and not use the API. Here are a few more static methods that can be inserted into that code to provide simple file moving or copying functionality from a remote client. Documentation about the move RPC method can be found here. To copy rather than move the file, change the doCopy flag to true. An alternative method for remotely moving a file from one document library to another location is to use the copy web service which is documented on MSDN here. For local SharePoint calls, the appropriate API method is SPListItem.CopyTo or SPFile.MoveTo.

Update: You can download a comprehensive c# class library to automate RPC calls - including moving files (even between web sites or servers). See this blog post for more information.

        public static bool Move(string oldUrl, string newUrl)
        {
            string result = "";
            string webUrl = GetWebURL(oldUrl);
            oldUrl = oldUrl.Substring(webUrl.Length + 1);
            newUrl = newUrl.Substring(webUrl.Length + 1);
            return Move(webUrl, oldUrl, newUrl, out result);
        }
 
        public static bool Move(string webUrl, string oldUrl, string newUrl, out string result)
        {
            EnsureFolders(webUrl, newUrl);
            string renameOption = "findbacklinks";
            string putOption = "overwrite,createdir,migrationsemantics";
            bool doCopy = false;
            string method = "method=move+document%3a12.0.4518.1016&service_name=%2f&oldUrl={0}&newUrl={1}&url_list=[]&rename_option={2}&put_option={3}&docopy={4}";
            method = String.Format(method, oldUrl, newUrl, renameOption, putOption, doCopy.ToString().ToLower());
            try
            {
                using (WebClient webClient = new WebClient())
                {
                    webClient.Credentials = CredentialCache.DefaultCredentials;
                    webClient.Headers.Add("Content-Type", "application/x-vermeer-urlencoded");
                    webClient.Headers.Add("X-Vermeer-Content-Type", "application/x-vermeer-urlencoded");
                    result = Encoding.UTF8.GetString(webClient.UploadData(webUrl + "/_vti_bin/_vti_aut/author.dll", "POST", Encoding.UTF8.GetBytes(method)));
                    if (result.IndexOf("\n<p>message=successfully") < 0)
                        throw new Exception(result);
                }
            }
            catch (Exception ex)
            {
                result = ex.Message;
                return false;
            }
            return true;
        }
 
        public static void EnsureFolders(string rootUrl, string folderUrl)
        {
            StringBuilder sb = new StringBuilder(rootUrl.TrimEnd('/'));
            string[] segments = folderUrl.Split('/');
            for (int i = 0; i < segments.Length - 1; i++)
            {
                sb.Append("/");
                sb.Append(segments[i]);
                CreateFolder(sb.ToString());
            }
        }
 
        public static bool CreateFolder(string folderURL)
        {
            try
            {
                WebRequest request = WebRequest.Create(folderURL);
                request.Credentials = CredentialCache.DefaultCredentials;
                request.Method = "MKCOL";
                WebResponse response = request.GetResponse();
                response.Close();
                return true;
            }
            catch (WebException)
            {
                return false;
            }
        }

Tuesday, December 4, 2007

Redirecting to an Error Page

The SPUtility.TransferToErrorPage method is a convenient way to redirect users to an error page in the event that an unhandled exception occurs. This post (from Wellington no less!) covers this method and a few other static methods from the SPUtility class well. One thing worth pointing out is the use of the second overload to the TransferToErrorPage method. This allows a link to appear in the message so that users can be redirected to another page (for example to a long running job status list item for more information on the error). To use this override, pass the link text and link url and then include a '{0}' (like the composite format syntax) in your message where you need the link to appear.

Wednesday, November 14, 2007

Upload a File to a SharePoint Document Library - Part II

In Part I I posted code that used the SharePoint lists service to help in uploading files with meta data to a sharepoint document library. Here is another variation, this time using FrontPage Server Extensions and RPC. Note that this code won't create more than one new folder - if this is a problem you may want to ensure the full folder path with code like that from the first article.

Update: You can download a comprehensive c# class library to automate RPC calls - including uploading files to a SharePoint document library. See this blog post for more information.

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;
using System.Web;
 
namespace DevHoleDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, object> properties = new Dictionary<string, object>();
            properties.Add("Title", "Test Title");
            //Create or overwrite text file test.txt in 'Docs' document library creating folder 'Test Folder' as required.
            DocLibHelper.Upload("http://localhost/Docs/Test Folder/test.txt", System.Text.Encoding.ASCII.GetBytes("Test text."), properties);
        }
    }
 
    static class DocLibHelper
    {
        static string EncodeMetaInfo(Dictionary<string, object> metaInfo)
        {
            if (metaInfo == null) return "";
            StringBuilder sb = new StringBuilder();
            foreach (KeyValuePair<string, object> kvp in metaInfo)
            {
                if (kvp.Value != null)
                {
                    string fieldName = kvp.Key; // note: field names are case sensitive
                    switch (fieldName)
                    {
                        case "Title":
                            fieldName = "vti_title";
                            break;
                    }
                    string data = EscapeVectorChars(kvp.Value.ToString());
                    string dataTypeCode = "S";
                    switch (kvp.Value.GetType().FullName)
                    {
                        case "System.Boolean":
                            dataTypeCode = "B";
                            break;
                        case "System.DateTime":
                            data = ((DateTime)kvp.Value).ToString("s") + "Z";
                            break;
                    }
                    sb.AppendFormat("{0};{1}W|{2};", fieldName, dataTypeCode, data);
                }
            }
            return HttpUtility.UrlEncode(sb.ToString().TrimEnd(';'));
        }
 
        static string EscapeVectorChars(string value)
        {
            StringBuilder sb = new StringBuilder();
            foreach (char c in value)
            {
                switch (c)
                {
                    case ';':
                    case '|':
                    case '[':
                    case ']':
                    case '\\':
                        sb.Append("\\");
                        break;
                }
                sb.Append(c);
            }
            return sb.ToString();
        }
 
        public static string GetWebURL(string url)
        {
            try
            {
                url = url.Substring(0, url.LastIndexOf("/"));
                try
                {
                    using (WebClient webClient = new WebClient())
                    {
                        webClient.Credentials = CredentialCache.DefaultCredentials;
                        webClient.Headers.Add("Content-Type", "application/x-vermeer-urlencoded");
                        webClient.Headers.Add("X-Vermeer-Content-Type", "application/x-vermeer-urlencoded");
                        byte[] data = Encoding.UTF8.GetBytes("method=open+service%3a12.0.4518.1016&service_name=%2f");
                        string result = Encoding.UTF8.GetString(webClient.UploadData(url + "/_vti_bin/_vti_aut/author.dll", "POST", data));
                        if (result.IndexOf("\n<li>status=327684") == -1)
                            return url;
                        throw new Exception();
                    }
                }
                catch
                {
                    return GetWebURL(url);
                }
            }
            catch
            {
                return null;
            }
        }
 
        public static bool Upload(string destinationUrl, byte[] bytes, Dictionary<string, object> metaInfo)
        {
            string result = "";
            string webUrl = GetWebURL(destinationUrl);
            string documentName = destinationUrl.Substring(webUrl.Length + 1);
            return Upload(webUrl, documentName, bytes, metaInfo, out result);
        }
 
        public static bool Upload(string webUrl, string documentName, byte[] bytes, Dictionary<string, object> metaInfo, out string result)
        {
            string putOption = "overwrite,createdir,migrationsemantics";  // see http://msdn2.microsoft.com/en-us/library/ms455325.aspx
            string comment = null;
            bool keepCheckedOut = false;
            string method = "method=put+document%3a12.0.4518.1016&service_name=%2f&document=[document_name={0};meta_info=[{1}]]&put_option={2}&comment={3}&keep_checked_out={4}\n";
            method = String.Format(method, documentName, EncodeMetaInfo(metaInfo), putOption, HttpUtility.UrlEncode(comment), keepCheckedOut.ToString().ToLower());
            List<byte> data = new List<byte>();
            data.AddRange(Encoding.UTF8.GetBytes(method));
            data.AddRange(bytes);
            try
            {
                using (WebClient webClient = new WebClient())
                {
                    webClient.Credentials = CredentialCache.DefaultCredentials;
                    webClient.Headers.Add("Content-Type", "application/x-vermeer-urlencoded");
                    webClient.Headers.Add("X-Vermeer-Content-Type", "application/x-vermeer-urlencoded");
                    result = Encoding.UTF8.GetString(webClient.UploadData(webUrl + "/_vti_bin/_vti_aut/author.dll", "POST", data.ToArray()));
                    if (result.IndexOf("\n<p>message=successfully") < 0)
                        throw new Exception(result);
                }
            }
            catch (Exception ex)
            {
                result = ex.Message;
                return false;
            }
            return true;
        }
    }
}
 

Monday, November 5, 2007

Provide Status Updates for Long Running Operation Jobs

A simple way to give users some feedback on the progress status of long running jobs is provided by the Microsoft.SharePoint.Publishing assembly. When a class that inherits from the LongRunningOperationJob class has its start method invoked, a list item is added to the Long Running Operation Status list in the root web. Providing a link to the LongRunningOperationProgress.aspx page along with the guid of this list item will display an update page with a progress bar similar to that shown below:

Long Running Job Status Page

A LongRunningOperationJob might not necessary be designed to run as a SPJobDefinition timer job, although there's no reason you couldn't combine the two to provide more granular information than the _admin/ServiceRunningJobs.aspx page provides.

Some example code follows. Before running it, make sure that your site collection includes a list named 'Long Running Operation Status' at the root web. This list is created when you install the Office SharePoint Server Publishing Infrastructure feature at the Site Collection Features level. The list apparently isn't removed when this feature is uninstalled so it's not necessary to have the Publishing Infrastructure feature activated if you don't want it to be - just activate then deactivate it.

The Microsoft.SharePoint.Publishing assembly can be found here:
%Program Files%\Microsoft Office Servers\12.0\Bin\Microsoft.SharePoint.Publishing.dll

using System;
using System.Diagnostics;
using System.Threading;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Publishing.Internal;
 
namespace DevHoleDemo
{
    class Program
    {
        const string STATUS_LIST_NAME = "Long Running Operation Status";
        const string PROGRESS_PAGE_URL = "/_layouts/LongRunningOperationProgress.aspx";
        static void Main(string[] args)
        {
            string webURL = "http://localhost/";
            using (SPSite site = new SPSite(webURL))
            {
                using (SPWeb web = site.RootWeb)
                {
                    LongRunningJob longRunningJob = new LongRunningJob();
                    longRunningJob.Title = "Demo Long Running Job";
                    longRunningJob.TotalOperationsToBePerformed = 30;
                    longRunningJob.RedirectWhenFinished = true;
                    longRunningJob.NavigateWhenDoneUrl = web.Url + "/" + STATUS_LIST_NAME;
                    longRunningJob.Start(web);
                    Process.Start("iexplore.exe", string.Format("{0}{1}?JobId={2}", web.Url, PROGRESS_PAGE_URL, longRunningJob.JobId));
                    LongRunningOperationStatus jobStatus;
                    do
                    {
                        jobStatus = LongRunningOperationStatus.GetJob(site, longRunningJob.JobId);
                        Console.WriteLine(jobStatus.StatusDescription);
                        Thread.Sleep(1000);
                    } while (jobStatus.Status != LongRunningOperationJob.OperationStatus.Successful);
                }
            }
        }
    }
 
    class LongRunningJob : LongRunningOperationJob
    {
        public override void DoWork()
        {
            for (this.OperationsPerformed = 0; this.OperationsPerformed < this.TotalOperationsToBePerformed; this.OperationsPerformed++)
            {
                this.StatusDescription = string.Format("im in ur long running job, doing ur work {0} of {1}...", this.OperationsPerformed, this.TotalOperationsToBePerformed);
                this.UpdateStatus();
                Thread.Sleep(1000);
            }
        }
    }
}