Tuesday, 18 June 2013

ProcessBatchData Helper Class

There are a few ways to add multiple rows to a SharePoint list. The iterative approach (i.e.: line by line) is simple, but this pattern will fail a CAF report. The better way to get the data up is loading a batch file using the ProcessBatchData method on your trusty SPWeb object. (The official blurb on the method is here.)

The problem with this is that is requires a well formatted XML string. The easiest way to accomplish this is to writer a helper class and it is quite a life saver.

Here is the class, followed by a sample of how to use the code.

   public class ProcessBatchDataHelper
    {
        private const string ProcessBatchStartXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
        private const string ProcessBatchRowsStartXml = "<ows:Batch OnError=\"Continue\">";
        private const string ProcessBatchEndXml = @"</ows:Batch>";

        private List<BatchProcessMethod> _methods = new List<BatchProcessMethod>();
        private int _methodId;

        public ProcessBatchDataHelper()
        {
        }

        private string GetMethodId()
        {
            return String.Format("M{0}", ++_methodId);
        }

        public BatchProcessMethod NewAddMethod(Guid listId)
        {
            var method = new BatchProcessAddMethod(GetMethodId(), listId.ToString("D"));
            _methods.Add(method);
            return method;
        }

        public BatchProcessMethod NewUpdateMethod(Guid listId, string itemId)
        {
            var method = new BatchProcessUpdateMethod(GetMethodId(), listId.ToString("D"), itemId);
            _methods.Add(method);
            return method;
        }

        public void NewDeleteMethod(Guid listId, string itemId)
        {
            var method = new BatchProcessDeleteMethod(GetMethodId(), listId.ToString("D"), itemId);
            _methods.Add(method);
        }

        public string ToCAML()
        {
            StringBuilder caml = new StringBuilder(10000);
            caml.AppendLine(ProcessBatchDataHelper.ProcessBatchStartXml);
            caml.AppendLine(ProcessBatchDataHelper.ProcessBatchRowsStartXml);

            _methods.ForEach(method => caml.AppendLine(method.ToCAML()));

            caml.AppendLine(ProcessBatchDataHelper.ProcessBatchEndXml);

            return caml.ToString();
        }
    }

    public enum BatchProcessCommand { Save, Delete };

    public abstract class BatchProcessMethod
    {
        private const string ProcessBatchSetVarXml = "\t\t<SetVar Name=\"urn:schemas-microsoft-com:office:office#{0}\">{1}</SetVar>";
        private const string ProcessBatchMethodEndXml = "\t</Method>";
        private const string ProcessBatchMethodStartXml = "\t<Method ID=\"{0}\"><SetList>{1}</SetList><SetVar Name=\"ID\">{2}</SetVar><SetVar Name=\"Cmd\">{3}</SetVar>";

        private string _methodId;
        private string _listId;
        private string _itemId;
        private BatchProcessCommand _cmd;

        private List<string> _setVars = new List<string>();

        protected BatchProcessMethod(string methodId, string listId, string itemId, BatchProcessCommand cmd)
        {
            _methodId = methodId;
            _listId = listId;
            _itemId = itemId;
            _cmd = cmd;
        }

        public void SetVar(string varName, string val)
        {
            _setVars.Add(String.Format(BatchProcessMethod.ProcessBatchSetVarXml, varName, val ?? String.Empty));
        }

        public string ToCAML()
        {
            StringBuilder caml = new StringBuilder(5000);
            caml.AppendLine(String.Format(BatchProcessMethod.ProcessBatchMethodStartXml, _methodId, _listId, _itemId, _cmd.ToString()));

            _setVars.ForEach(var => caml.AppendLine(var));

            caml.AppendLine(BatchProcessMethod.ProcessBatchMethodEndXml);

            return caml.ToString();
        }
    }

    public sealed class BatchProcessAddMethod : BatchProcessMethod
    {
        internal BatchProcessAddMethod(string methodId, string listId)
            : base(methodId, listId, "New", BatchProcessCommand.Save)
        {
        }
    }

    public sealed class BatchProcessUpdateMethod : BatchProcessMethod
    {
        internal BatchProcessUpdateMethod(string methodId, string listId, string itemId)
            : base(methodId, listId, itemId, BatchProcessCommand.Save)
        {
        }
    }

    public sealed class BatchProcessDeleteMethod : BatchProcessMethod
    {
        internal BatchProcessDeleteMethod(string methodId, string listId, string itemId)
            : base(methodId, listId, itemId, BatchProcessCommand.Delete)
        {
        }
    }

The following code does a batch update for the list "list" to the second list "list2" (yes, I know the names are AWESOME). The portion to be aware of is the item loop - I am only setting the Title column, but it can easily be extended. I have added a helpful comment to point you in the right direction.

ProcessBatchData requires the internal column name - I have created a helper method called 'InternalName', which will extract the name from the title.

NOTE: This method  does not take into account the size of the source list - if there are 1000000 rows, it will be done in a single batch. It would probably be best to send the records in batches of 100.

       private void ProcessBatch()
        {
            string sourceListName = "list";
            string targetListName = "list2";
           
            using (SPSite site = new SPSite(@"http://mysite/"))
            {
                using (SPWeb web = site.RootWeb)
                {
                    SPList sourceList = web.Lists[sourceListName];
                    SPList targetList = web.Lists[targetListName];

                    ProcessBatchDataHelper cmd = new ProcessBatchDataHelper();

                     SPListItemCollection items = sourceList.Items;
                    foreach (SPListItem item in items)
                    {
                        BatchProcessMethod method = cmd.NewAddMethod(targetList.ID);

                        string titleField = InternalName("Title", sourceList);
                        string titleValue = item[titleField].ToString();

                        method.SetVar(titleField, titleValue);
                        // Add additional columns here
                        // method.SetVar(secondFieldInternalName, secondFieldValue);
                    }

                    web.ProcessBatchData(cmd.ToCAML());
                }
            }
        }

        private string InternalName(string displayName, SPList sourceList)
        {
            SPFieldCollection fields = sourceList.Fields;
            if (fields.ContainsField(displayName))
            {
                if (string.Compare(displayName, "Title", true) == 0)
                    return "Title";
                else
                    return fields[displayName].InternalName;
            }

            return string.Empty;
        }

Sunday, 16 June 2013

Token Replacement in Visual Studio

I was working on a simple WCF service, including a generic handler (.ashx). My code looked good until I tried to access the url - it started complaining that it did not know what '$SharePoint.Project.AssemblyFullName$' was.

A little digging ( here and here ) revealed at VS will not replace its tokens in all file types - you may need to specify them in the project file. Generic handlers are one of those types.

So, first I unloaded the project and added the following to the property group:

<PropertyGroup>
 <TokenReplacementFileExtensions>ashx</TokenReplacementFileExtensions>
</PropertyGroup>

I then reloaded the project and redeployed. Too easy.


Create a new SharePoint Farm: Local accounts should only be used in stand alone mode

I was creating my new VM and installed a SharePoint Farm on the local machine. Easy enough. All was good until I tried to run the configuration wizard.

That is when it all when wrong. I got the fantastic error message:

Local accounts should only be used in stand alone mode

Some simple searching brought my to this post ( here ) that gave me the solution. Basically, you need to run the installation in PowerShell (using New-SPConfigutionDatabase), as it exposes properties that are not available through the wizard. Once the database has been created, SharePoint thinks it has a Farm and the configuration wizard can continue on its merry way.

Its pretty simple when you know how ....




Tuesday, 4 June 2013

SharePoint 2010 Code Samples

I have come across the following code samples for SP 2010 development on the Microsoft site. It is full of helpful code snippets and solutions that will probably save me a lot of searching.

The samples are here