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:

Thursday 10 November 2011

QueryOverride and its quirks - use with caution!

This post deals with the quirks involved with the QueryOverride property in a Content Query Editor WebPart. Suffice it to say when they say "override" they mean it. It overrides almost everything you set for a Content Query Editor Web Part in your UI and if you are unprepared this is a massive PITA to find and fix. I was, naturally, unprepared and had to find and fix it.

It started with a request to filter an existing UI so that news items lingering longer than a month would not be displayed. A bit of digging and I soon learned that the "Modify Shared Web Part" in the UI cannot calculate back from [Today], only a fixed date, so I would have to select Export on the menu and save to my own hard drive to edit the CAML in notepad. Naïve as I was, I thought it would be simple enough to replace the QueryOverride line with:

<property name="QueryOverride" type="string" ><![CDATA[<Where><And><Geq><FieldRef Name='Modified'/><Value Type="DateTime"><Today OffsetDays="-30"/></Value></Geq></Where>]]></property>
OK. Well I uploaded it to the site collection web part gallery, recreated my web part on the page to point to it, and that seemed to work. Last April's stuff was no longer appearing on the list. Except that one of my colleagues mentioned, "Hey, the order looks funny". I had a look and indeed she was right. Instead of descending order, it went from earliest first. So I went "Edit Page" and modified the web part to switch from Ascending to Descending, clicked "Apply" and hey presto…it switched right back to "Ascending" again! I tried this several times, with the same result. More googling and nail biting. Eventually I found that I needed to specify not only WHAT I wanted back, but what order I wanted it in. So my QueryOverride line became:

<property name="QueryOverride" type="string" ><![CDATA[<Where><Geq><FieldRef Name='Modified'/><Value Type="DateTime"><Today OffsetDays="-30"/></Value></Geq</Where><OrderBy><FieldRef Name="Created" Nullable="True" Type="DateTime" Ascending="FALSE"/></OrderBy>]]></property>

RIGHT. Upload, delete web part, recreate. So now we had the correct items appearing in the correct order. Happy Days. Except then the user said, "Hey, we have another problem".

The CEWP I was editing was pulling all items from all site collections on the portal that had a particular page content type. One library had items which were not to be included in the main list. It had a different page content type. Everyone knew about this other page content type and used it. The main news webpart had always ignored it because it was pointing to a different content type. Everything had been fine. Up till the time I'd put in my QueryOverride property, when it had all gone to dog doodoo. The items from the not-to-be-included library were very much included, and what was I going to do about it, eh?

I looked at the CAML of the two web parts, the good one and the bad one (i.e. mine)  - I copied them both into Excel and eyeballed them there, for God's sakes - and could not see what the problem was. Both web parts had their respective content types clearly specified in the Presentation section. There could be no ambiguity. So what was going on?

I finally discovered that QueryOverride doesn't care what you put in your content types. Once I put in the "Modified" date in the queryoverride, I was toast. It just hoovered up everything from everywhere REGARDLESS of what I specified for content types in the UI and what was written in the CAML. I realised I would have to tell the bloody thing for once and for all - don't use that &*(^ing content type. So now here is the final all-singing, all-dancing QueryOverride line I had to use. Oh and note the location of the <And> tags. I stuffed that up the first time and wondered why the logic wasn't working...

<property name="QueryOverride" type="string" ><![CDATA[<Where><And><Geq><FieldRef Name='Modified'/><Value Type="DateTime"><Today OffsetDays="-30"/></Value></Geq><Neq><FieldRef Name="ContentType"/><Value Type="Text">[My Little Content Type]</Value></Neq></And></Where><OrderBy><FieldRef Name="Created" Nullable="True" Type="DateTime" Ascending="FALSE"/></OrderBy>]]></property>

Please note that I have put the content type name in square brackets. I don't think my employer would care for me to put in the real content type name!

All right, good to go at last, surely! But no, not yet. The news items weren't displaying the way they were in the last web part, where we'd seen a nice little teaser under the headline. So I stuck the thing into Excel (again) to compare and amended the following CAML parameters:

<property name="ParameterBindings" type="string" null="true" />
<property name="CommonViewFields" type="string">PublishingPageContent</property>
<property name="NoDefaultStyle" type="string" null="true" />


Finally! It worked!