Wednesday, September 30, 2009

Dear SharePoint Team: ItemDeleted? Seriously?

ItemDeleted - a would-be handy event receiver, but in reality no more useful than a box full of used matches.

An analogy, if I may, to explain the state of affairs

While away on business, you're phoned up by your home town police, and given the brief message:
- Someone very dear to you just passed away.
Caught slightly off guard, but immediately inquisitive, you might ask:
- Who?!
Which is rewarded with a mere:
- I don't know.
- What do you mean "I don't know"? For God's sake man, someone just died, and you can't tell me who?
- No.
- Well do you know anyone who could know, then?
- No.
- Great .. Thanks a heap.

What I'm saying is ...

ItemDeleted knows *nothing* about the item that was deleted, apart from its now invalid index into the list. While the event receiver properties does contain the fields BeforeProperties and AfterProperties, the former of which one would expect to contain some information about the state of affairs *prior* to the delete taking place; both are null.

Next up, there's ItemDeleting. After all, that signals the same possible state change, and unlike it's dimwitted brother 'Deleted, it *does* have the BeforeProperties initialized. Upon discovering this, you might decide to use this receiver to process item deletions, such as in my current project - to signal item changes across farm boundaries. What you're not considering, then, is that ItemDeleting is just an indication of what might happen. Any number of receivers following yours may set SPItemEventProperties.Cancel to abort the delete, thus possibly invalidating the state of your application (parts of it thinking an item is deleted, other parts knowing it's not).

So what can we do, then?

Parse the RecycleBin?

Possible, but even if we did iterate all items in there (possibly *a lot* of overhead), we don't have any idea at all what the items title, name or guid might have been.

Store event handler instance / static data in ItemDeleting, then check in ItemDeleted?

Unlike the RecycleBin, this would work, and it would give a reliable indication of what item was deleted, but it'd cost an arm and a leg efficiency-wise. Event handler instance data is a no-go, I should add, since event handler classes are initialized once per call to a receiver function (ugh). You could use an interlocked static field, but 1. it would be very inefficient in any environment with more than zero users (locking and unlocking the field for each call), 2. I would grant you *no* guarantees in a multi-frontend farm setup, or even a multi-worker process environment.

Store state info in the SPItemEventProperties.ReceiverData from ItemDeleting, then process it in ItemDeleted?

Once again: would require inefficient thread interlocking, possibly not visible across frontends / processes, and certainly the most ugly solution you'd ever put into production code. You should be ashamed.

Store data in [web properties / custom list / user profile / own database / an usb drive] from ItemDeleting, then process it in ItemDeleted?

Sure, that would work, and all apart from the usb drive it would probably be accessible from all processes / frontends. Your major pain in the neck here, though, is the lingering fact that ItemDeleted *may not be executed*. Imagine installing some third party WillProtectYourDataAndCrap wsp solution, which essentially will cancel all ItemDeleting events. Then imagine your custom table / properties / whatever, suddenly containing 50.000 could-once-have-been deleted entries, which were never processed or removed since ItemDeleted never executed. At that point: welcome a custom SPJobDefinition to clean up every x minutes. Also give yourself complementary slap on the back, since you've just implemented the worlds most over-engineered and likely-to-break-down-horribly "What was just deleted?"-mechanism.

Man, wouldn't it have been great if ItemDeleted could just tell you what was just deleted?

Friday, September 18, 2009

Filling a SharePoint web service gap: Fetching current username, using client-side only jQuery Ajax

I'm stumped as for why no such service was included in the first place, seeing as the code is a one-liner, and I imagine it quite useful to most service clients.

Having first settled on a custom service (code here) to deliver the user name, deploying that isn't always an option. If you've only got client-side access to the portal, you *need* to do it with a script..

So I came up with this solution, which should work in most cases:

window.userInfo =
{
/* Public interface */
getCurrentUsername: function(callback)
{
$.get(
"/_layouts/userdisp.aspx?Force=True",
null,
function(data)
{
var username = window.userInfo._findUsernameFieldText(data);
callback(username);
}, "html");
},

/* Private interface */
_findUsernameFieldText: function(pageData)
{
var fields = $(pageData).find("table.ms-formtable td#SPFieldText");
for(var i = 0; i < fields.length; ++i)
{
field = fields.get(i);
for(node = field.firstChild;
node.nextSibling != null;
node = node.nextSibling)
{
if(node.nodeType == 8 && /FieldInternalName=\"UserName\"/.test(node.nodeValue))
return $(field).text().replace(/(^[\s\xA0]+|[\s\xA0]+$)/g, '');
}
}
return null;
}
};

What I realized, as I pondered this yesterday, is that there are obviously a lot of pages in the portal which lists the current user's info - including the user profile page. Extracting the username should be no harder than asynchronously loading the page (using jQuery) and parsing the resulting form looking for a field commented with 'FieldInternalName="UserName"'.

Should the userdisp page be violently customized, this would be less likely to work, but being a _layouts page; such a customization is unlikely.

Granted the above code, you can test it by pasting that (in a script tag) plus the following into the html source of a content editor web part:

<script src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("jquery", "1");
google.setOnLoadCallback(function()
{
userInfo.getCurrentUsername(function(usr) { alert("[" + usr + "]"); });
});
</script>


Full source code download here.

Thursday, September 17, 2009

Forcing SharePoint into asynchronous AJAX-like submission, using jQuery

A while ago I posted a screencast on Twitter, of me navigating around a WSS 3.0 document library, without any synchronous reloads (full page refreshes): changing sort settings, creating folders and deleting files - all updated in place. If you didn't catch that, here's the screencast:



The demo looks pretty cool (he said confidently), and I feel that these are features SharePoint is lacking. With SharePoint 2010, this will all have been improved majorly, so it's admittedly not the most useful feature to be adding now - a mere few months before the release of '10. For the sake of (mis)using JavaScript and jQuery, though - let's stride on!

The code behind the demo is pretty intrusive - I'll be the first to admit that - and it's in no way complete. What it does, is override the usual SharePoint document library submit behavior, with a jQuery asynchronous load. The loaded data, which would be the page returned from the server, is chopped up and updated at the relevant spots on the existing page. If the user opens a folder in a document library, the asynch call loads the page with the view of the folder, then strips all the stuff which isn't the view of the folder, and replaces the current folder view: straight forward DOM stuff.

For this hack to (kinda) work, I also override some standard SharePoint APIs which returns state information (e.g. which document library folder we're currently viewing). I spent a minimal amount of time completing this, so I have no doubt that some state info will be missing or wrong in the code below, but it does seem to work fairly well for folder navigation, creation and item deletion. Sorting, however, is another matter entirely.

So all in all, there are quite a few kinks. The create folder popup is included mostly for show, and looks horrible. I didn't adapt this to work with the upload form, although that improvement would be pretty simple to complete. One of the more prominent issues, however, is the scripts which are returned along with the asynchronous loads. IE8 freaks out somewhat by these, and throws an error at you when you e.g. delete an item in a library. The fix is to ignore scripts in the parts of the page we keep (the view of the folder, etc.), but that's another thing I didn't look into.

All in all, I consider this a curiosity, rather than something of actual value. I'm positive someone with more time on their hands than me could improve it a ton, but then again - why bother with SharePoint 2010 coming up :-)

Excuses aside, here's the important part of the script:

var wssHook =
{
/* Public interface */

setupWSSHooks: function()
{
this.hookWSSApi("FilterFieldV3");
this.hookWSSApi("GetSource");
this.hookWSSApi("GetUrlKeyValue");
SubmitFormPost = this.hookedSubmitFormPost;
STSNavigate = this.hookedSTSNavigate;
},

/* Private interface */

hookWSSApi: function(api)
{
this[api] = window[api];
var body = window[api].toString();
eval("window[api]=function" + body.substring(body.indexOf("(")).replace("window.location.href", "window.realurl?window.realurl:window.location.href"));
},

hookedSubmitFormPost: function(url, forcesubmit, getonly)
{
window.realurl = url;
var postvars = {};
if(!getonly)
{
$(document.forms[MSOWebPartPageFormName]).find("input").each(function(id, element) { postvars[element.name] = element.value; });
}
else
{
postvars = "";
}
$("table#MSO_ContentTable>tbody").load(url + " table#MSO_ContentTable>tbody", postvars,
function(resp, status, req)
{
var location = req.getResponseHeader("Location");
if(location)
{ window.realurl = location; }
});
},

hookedSTSNavigate: function(url)
{
var shown = false;
var dialog = $("<div></div>");
dialog
.dialog({
width: "80%",
height: "300",
modal: true,
close: function(event, ui) { dialog.remove(); SubmitFormPost(window.location.href, false, true); }
})
.append(
$("<iframe frameborder='0' width='100%' height='100%' src='" + url + "' />")
.load(function(){
if(shown) { dialog.dialog('close'); return; }
shown = true;
var c = $(this).contents();
var ctrls = c.find("td.ms-bodyareaframe>table").clone();
ctrls.find("script").remove();
c.find("table#.ms-main:first").remove();
c.find("form#aspnetForm").append(ctrls);
})
);
}
}

You can download the entire thing, including jQuery / jQuery-UI loads from the Google CDN, here.

If you want to give it a test run in your own portal; head to a document library, add a content editor web part and add the code downloaded from the link above as the html source.

Dealing with abducted SharePoint features

If you've ever gotten an error message when deploying a feature with stsadm or WSPBuilder, resembling the following:
The feature '...' uses the directory "NewFeatureName" in the solution. However, it is currently installed in the farm to the directory "OldFeatureName". Uninstall the existing feature before you install a new version of the solution.
... or you've loaded up SharePoint Manager 2007, checked the feature definitions, only to find one or more features without the green "ALL IS OK" icon, an error reading "Object reference not set to an instance of an object" when you try to open them, and no option to delete.

... or you've otherwise been slapped a message indicating that you've once had a feature installed somewhere, but although its files are now gone, SharePoint won't allow you to deploy a new one with the same guid.

The solution is relatively simple, and can be automated with a lookup of the local farm's feature definitions, deleting those who throw an "not found" error when you attempt to access its properties. Kind as I am, I've already thrown this together for you, and made it available as a download here (Grep.SharePoint.Tools.CleanFeatures). The source code follows below:

namespace Grep.SharePoint.Tools.CleanFeatures
{
using System;
using System.Linq;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

internal class Program
{
private static void Main(string[] args)
{
if (!args.Contains("/D"))
{
Console.Out.WriteLine("Listing missing features. Invoke with /D to delete as they are found.");
}
else
{
Console.Out.WriteLine("Deleting missing features.");
}

SPFarm farm = SPFarm.Local;
foreach (SPFeatureDefinition feature in farm.FeatureDefinitions)
{
try
{
SPFeaturePropertyCollection p = feature.Properties;
}
catch (SPException e)
{
if (e.ErrorCode == -2146232832)
{
Console.Out.WriteLine("Not found: {0} ({1})",
feature.Id,
feature.RootDirectory.Substring(feature.RootDirectory.LastIndexOf(@"\") + 1));
if (args.Contains("/D"))
{
feature.Delete();
}
}
}
}
Console.Out.WriteLine("Done");
}
}
}

Find who's not mutually following you on Twitter, using jQuery Ajax / JSON

Admittedly not the most useful use of the Twitter API, nor of jQuery, but nevertheless here it is.

Two different APIs are used; one to check the statuses of all friends of user X, which supplies us with 1. friends, 2. screen names and 3. user ids. The second API retrieves all user ids of the user X's followers. The two resulting lists of these APIs are diffed against each other, and voila - you've got the illoyal ones.

No username or password required to do this (unlike some other services I've seen), as long as your profile isn't protected (in which case you can easily modify it to supply a user / pass). It's completely harmless to run, as long as you don't hammer the Twitter server. The code will spend one API call (out of your total 150 per hour) per 100 friends of the user you target, and another single call to fetch all the followers. If you have more than 10k friends: don't run this.

Here's a fully functional demo. Feel free to try it on your own screen name. Again, no password or cross site scripting takes part here, so it's completely harmless. Input a screen name in the input box, and click the "check" button.

If you want to toy around with the code, it can be found here.

Thursday, September 10, 2009

SharePoint logger bug workaround

So yesterday I blogged about a bug in the SharePoint tracelog code, which would result in a ridiculous and unnecessary exception, should you happen to create a site collection, with feature file deployment, from a WCF service. If you haven't read that - feel free to do that now: 'Tis a SharePoint bug, it is!

Anyhoo, after giving up on having it fixed by the powers that be anytime soon, and not wanting to be too intrusive (ie. patching standard code, or injecting a hotfix at runtime), I went with a much more ... logical ... solution.

Allow me to demonstrate.

While this approach from yesterday didn't work:
Click it to view larger version.

This most certainly does:


In layman's terms: In the WCF service I now - rather than create the site coll directly - 1. Create a new thread, which creates the site coll, 2. Wait for the thread to end, and 3. Return. Makes perfect sense (actually it does). The stack rewind done in the trace api will stop at the entry into the worker thread, and thus never get to the DynamicMethod in WCF, which breaks it all apart.

Time for coffee.

SharePoint Conference 2009, I'm coming for you!

Last year it was hosted in Seattle. I had a great time going there, and did catch some interesting presentations, but can't say that the technical content brought me to bits in awe. This year, with the community and contributors having matured for even longer, I'm thinking it'll be a blast. With the upcoming release of SP2010, and the posibility of presentations on that as well, I really don't see how it can fail. If worst comes to worst, there's always poker and booze, right? Or juice and pancakes; whatever suits your fancy.

Should *you* find your way to Vegas at the same time (18th of October =>), feel free to slap me a tweet/mail, so we can formally exchange our hellos!

Wednesday, September 9, 2009

'Tis a SharePoint bug, it is!

I ran into this a few weeks ago, but had to delay dealing with it, for the sake of [somegoodreason]. Now I'm back on track, and I'm obviously going to whine about it to you.

The background for this problem is simple:

I create a site collection from a site definition, using code. This sitedef contains a couple of aspx files, which are put on the base of the new site's root web. To put the files there, I have Module tags in a feature's elements xml. The module tag contains a Name attribute, which is required, and used by SharePoint to look up the library it is to put the files in.

The thing is, though - ghostable files on the root of a web don't go in a library. So as SharePoint prepares the site, it fails to locate a non-existing library (which it wouldn't use anyway), and takes note of the error, but then carries on, as the library *really* wasn't required (told you).

Here's me explaining that by use of poor penmanship and malplaced creativity:
Click it to view larger version.


So while that scenario works out ok, what I'm actually doing, is calling the create-sitecollection code via a WCF service. And that plays out as follows (gore included):
Click it to view larger version.


The problem is rooted in SharePoint's logging code, which includes a routine to step back through the call stack, taking note of the where the failed call originated from. In this case, that would pass through the calls made by SharePoint to look up the library, deploy the file, open the elements, [...] and eventually into the service - which is internally in WCF an invoked DynamicMethod.

SharePoint's logger essentially does this:
StackFrame frame = trace.GetFrame(i);
builder.Append(frame.GetMethod().DeclaringType.ToString());
And that doesn't sit well with the DynamicMethod, which isn't really your run of the mill method (it's a dynamically compiled at runtime), and sure as batman doesn't have a DeclaringType. Calling the ToString() on a null reference ... not good at all.

Thus, SharePoint logs itself to bits and pieces, and I'm left none the wiser at the other end of the line.

So the moral of the story is:
  1. Null references are bad
  2. Assuming that all methods are declared somewhere, in a language which supports emitting and compiling code at runtime - also bad
  3. Logging events that aren't actually errors, and which no part of the application cares about either way, even when logging is turned off - well you'll figure that one out yourself.
Update: I made a new post (and obviously an illustration) on a workaround: here