Wednesday, 11 April 2012

Deleting Items on a List using SPWeb.ProcessBatchData()

I had a situation a week ago where I needed to write a script to delete all items in a list while maintaining the list structure. Now for those familiar with the SharePoint API, the obvious solution would be something like this:

foreach (SPListItem item in list.Items)
{
    item.Delete();
}


However theIEnumerator interface does not like deleting things while iterating through them because the list count will change while you're doing it. This is mighty confusing for an iterating loop so that's handled as an error and SharePoint sez, no can do, sorry. I tried using list.Items[0].Delete() as I thought that might work since there will always be at least one item in the collection, but no - when it got to the end of the collection it threw an error and all my old VB programming days of On Error Resume Next could not help me in my attempts to keep the code running after the error.


But anyway. All this has happily become a moot point since discovering the SPWeb.ProcessBatchData() command, since all I have to do is loop through every list in the web and issue a delete command. At first I got to the Microsoft help for this (always, always a bad idea. Sorry, Microsoft, but there it is) and got scared off by talk of OWS files and piles of XML. How and ever, after a bit of digging I found a great entry on Stack Overflow that assures me one does not need to be rooting around obscure XML files in the Twelve Hive in ungraceful fashion, but can build the XML on the fly as a string, feed it in to the ProcessBatchData command and hey presto, you're off:



foreach (SPList list in web.Lists)
   {
     try
       {
        //decrement loop so as not to confuse the iEnumerator interface
       Console.WriteLine("Deleting list items in following list : " + list.Title);
       SPListItemCollection splic = list.Items;
       StringBuilder batchString = new StringBuilder();
       batchString.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Batch>");
       foreach (SPListItem item in splic)
       {
         batchString.Append("<Method>");
         batchString.Append("<SetList Scope=\"Request\">" + Convert.ToString(item.ParentList.ID) + "</SetList>");
         batchString.Append("<SetVar Name=\"ID\">" + Convert.ToString(item.ID) + "</SetVar>");
         batchString.Append("<SetVar Name=\"Cmd\">Delete</SetVar>");
         batchString.Append("</Method>");
       }
       batchString.Append("</Batch>");
       web.ProcessBatchData(batchString.ToString());
    }
    catch { //whatever }

 }





Please note that I actually found this code somewhere on Stack Overflow, but have gone and lost the link. So Unknown Programmer, you have my greatest gratitude for this!

I believe there are Copy and Insert methods also but will have to check those out. I may have a requirement for these, due to SharePoint UI inflexibility, so stay tuned...

Monday, 13 February 2012

Retrieving MySite Info to a list via a User Value Field

Greetings. I have not updated this blog from some time because I was on leave all of January and was relaxing and enjoying myself. I follow SharePoint on my twitter feed and one of the tweets was "Do you dream about #SharePoint?" and I RT'ed the tweet with the additional comment, "You've got to be joking!" But now I am back and thoughts of such things return to my mind. This will be my first post for 2012, then. I spent quite some time struggling with this one. As far as I know it's the one place where the whole routine is present in one place so if anyone needs it they can use it.

If you want to get more info on a user, often you'll click on their MySite. This code effectively does the same thing. If you have a list or library that contains a "user or group" control, you can retrieve the user information from the field and you can use the UserProfileManager object to get all the info on that user. To do this, you're going to need some libraries that you might not normally have in your .NET store. So, first:

using System;
using Microsoft.SharePoint;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
using System.Web;

Now, if you can't see the Microsoft.Office.Server library in your usual reference store, don't panic. Go to your Twelve Hive folder and look for the ISAPI directory. The dll file should be in there. Add it in as a reference to your VB project.

Let's presume you are going to create a conventional event handler for the ItemAdded event and copy the resulting data into a text field called "Business Unit". We'll skip the event handler code as we've covered that already. I'm now going to use a privilege wrapper and get the string value of the user field. This was the bit that caused me by far the most grief. The ToString() method would not work on the user control field. Don't ask me why it wouldn't work, it just wouldn't. Kept returning nulls. So I did it like this:



Guid siteID = properties.SiteId;
Guid webID = properties.ListItem.Web.ID;
Guid listID = properties.ListId;
Guid listItemID = properties.ListItem.UniqueId;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
     using (SPSite site = new SPSite(siteID))
     {
          using (SPWeb web = site.OpenWeb(webID))
          {
                            
                SPList list = web.Lists[listID];
                SPListItem item = list.Items[listItemID];
                string userValue = (string)item["Reported By User"];
                SPFieldUserValue fieldUserValue = new SPFieldUserValue(web, userValue);
                            
                SPUser user = fieldUserValue.User;
                string department = GetBusinessUnit(user, site);            
                item["Business Unit"] = department;
                item.Update();
                        }
                    }
                });

//...
//subroutine to get profile info here


private string GetBusinessUnit(SPUser user, SPSite site)
{
    string busUnit = string.Empty;
    try
    {        
        ServerContext serverContext = ServerContext.GetContext(site);
        UserProfileManager upm = new UserProfileManager(serverContext);
        UserProfile profile = upm.GetUserProfile(user.LoginName);
        busUnit = profile["Department"].ToString();
    }
    catch (Exception ex)
    {
         //add your own error handling here
        busUnit = ex.Message + " : " + errorArgs;
    }
            
    return busUnit;

}

This I can guarantee will work because I ran it successfully myself. One note with error trapping: if you create the profile and use a field that has a null value in the user's mySite, there's nothing here to gracefully trap that at present, unless you don't mind your control displaying a honking big error message :) 

Wednesday, 21 December 2011

The Straw That Broke the CAML's back: Manipulating Content Type field in a view

This post will be brief. I wish my discovery had been so too!

Let's say you create an SPQuery object from the default view of a list. Then you deploy your query, putting the result into an SPItemCollection object. That done, you want to determine a certain sequence of events based on the content type of the specific item when looping through the collection.

So, trying various methods in vain:

SPQuery query = new SPQuery()
SPListItemCollection items = list.GetItems(query);

foreach (SPListItem item in items)
{
    if (item.ContentType.Name == "This line will throw an error)
      {
        //this condition never gets hit
      }
    
    if (item["ContentType"].ToString() == "So will this one")
      {
        //this one doesn't either
      }
    if (item.ContentType.ID == "you get the idea blah blah"
     ...

}

Kept getting "Cannot complete this action", "object not set to instance of an object", and when I tried Content Type ID some exception that had HRESULTS and big long hexy number and god knows what.

Then I thought - the view I ran the query from - I did display the content type on that - didn't I? I went in and ticked the ContentType field in the view so that it was visible?

Nope. I didn't. So it couldn't find it. D'oh! I hope reading this saves you the trouble I got into!

Tuesday, 15 November 2011

Creating and Installing a SharePoint Timer Job

Important: this should be carried out on a non-production server first and then refined before deploying to live!

I just created a timer scheduled job in SharePoint with the help of Some Code Off The Internet (TM) plus a few hard-earned lessons all of my own.

The important thing to realise about a timer job is that it's just another SharePoint feature plus dll with some feature overriding code in it. Nothing more, nothing less. When I download a project with all the components and setup stuff, I tend to just work with the actual .cs files in a class library, make the dll and then do it manually. That means I know what's going on.

The code I downloaded was here

(thank you Alexander Brütt)

But what I did rather than open the whole package as a Visual Studio item was to open the whole thing and drop all the build instruction files because I was deploying to another machine anyway. The three files you really need are

Job.cs
JobInstaller.cs
Feature.xml (which you will put somewhere separate in the FEATURES folder in the Twelve Hive anyway)

The rest is just detail. But don't forget the strong key. It saves time since that's the key in your feature.xml file. Also there are some items in the folder with dollar signs on them. They're for you to change the name. I called the Job class CustomSharePointJob and the Installer class CustomSharePointJobInstaller. Yes I was looking for an easy life there :)

Build the thing as a dll (forgetting the manifest xml and all that packaging stuff) and if there are build errors, chances are you need to conjure up a couple of GUIDs to identify the dll as the code has $guid somewhere, I think. It should eventually build ok.

OK, then GAC register your dll and make sure the info in the feature.xml file matches up. The Feature Receiver line should be the same as your components. Stick it into the Twelve Hive. Now fire up the command line, change dir to your twelve hive BIN folder and enter the following piece of sublime poetry:

stsadm -o installfeature  -filename CustomSharePointJob\Feature.xml

And then

stsadm -o activatefeature -filename CustomSharePointJob\Feature.xml -url http://mylittleserver

Hopefully it should give you the thumbs up. But just to make sure, open up SharePoint Central Admin and navigate to Operations - Timer Job Definitions. Your timer job should be there and its schedule should be Minutes.

OK, we want to deactivate it for a moment and do some work on it. So in stsadm

stsadm -o deactivatefeature -filename CustomSharePointJob\Feature.xml -url http://mylittleserver

The wonderful thing about this code is that it has event receivers for the feature deactivating, and the event triggers a delete. So you don't have to worry about taking the job off the list in sharepoint central admin, it's all taken care of.

Now in Visual Studio, have a look at the Job.cs folder. I put in instructions to send me an email when the job fired - just to make sure it worked ok. In order to do that I had to instantiate an SPSite object and SPWeb object which I did as per usual using a site url. But there is one important difference to note. The code here specifies a job lock type of SPJobLockType.ContentDatabase. This means that the job is designed to fire against every content database on that server. I was wondering why I was getting nine emails! Go into your Job.cs file and change that to SPJobLockType.Job and you are sorted, it will only fire the once.

Also, what else do I need to do. Well I don't want the damn thing running every 2 minutes, once a day is enough. I scoured the internet looking for something telling me how to set up a daily schedule and eventually found a solution which I've amended slightly to run at 6am. If, for testing purposes, you want to change that hour, just move up the BeginHour and EndHour properties:

SPDailySchedule schedule = new SPDailySchedule();
schedule.BeginHour = 6;
schedule.BeginMinute = 15;
schedule.BeginSecond = 0;
schedule.EndSecond = 15;
schedule.EndMinute = 15;
schedule.EndHour = 6;
myCustomSharePointJob.Schedule = schedule;
myCustomSharePointJob.Update();

I put in some code to do the thing I wanted to do on the site I wanted to do it on - this goes in the Execute method - recompiled the dll and GAC registered it once more. Ready to do battle - but wait -

Now this next step is very important

Save yourselves a good few hours of pain and do this now. Just when you've finished running gacutil from the command line, enter the following line:

net stop sptimerv3

(it will be sptimerv4 for you sharepoint 2010 heads)

And then straight afterwards type

net start sptimerv3

This stops and restarts the OWSTIMER.EXE process for sharepoint. (This is why not to do this on live.) The reason this has to be done is otherwise, the process will cache your old dll FOREVER. It doesn't matter if you re-register it seven times, doesn't matter if you delete the damn thing or restart the job or whatever - it will just keep caching it.

This has to be done every time the dll is recompiled!

Then - and only then - reactivate the feature by going into stsadm and entering the activatefeature command as described above. Then hopefully all should be well.

I would like to recommend the following links which are very helpful: