Tuesday, November 29, 2011

SharePoint Popup Scroll gone after autoSize

I’m writing this post after not finding anything about this problem in Google.

The issue

When you have a SharePoint 2010 popup dialog, that uses autoSize (like the new item / edit item forms in any SharePoint list), if you call “window.frameElement.autoSize()” second time, there is no scroll in the popup.

Say you have a field control or a web part that changes in height when the user interacts with it. In my case, I was working on a “repeating rows” field type, where the user can add as many rows to the field as he wants – much like in InfoPath.

When the user add new rows, the height of my popup changes, so I have to resize it.

Documentation is pretty clear, call window.frameElement.autoSize() and it should resize your dialog.

Apparently, this works well for the first time you call it, but for some reason if I call it second/third/ninth time – once the popup is too high the vertical scroll is gone.

Another issue is, that even when it does fit to the page – the top of the popup does not move up so you end up having to move the popup to the top of the page yourself.

This caused great frustration for some of our customers, and by extent – our product manager and support – and by extent – me!

The solution

I knew I was alone in this, since not only Google did not have any solution for this issue – I could not even find someone asking about it in any forums / mailing list.

So, since waiting for a Microsoft fix for this, which may or may not come eventually (after all this issue is introduced with custom development and not on OOB scenarios), I had to dig deep and get dirty with Microsoft JavaScript and popup DOM.

It wasn’t pretty, I tell you, but I did find lots of cool stuff, like a function that calculates the width of the scroll bars in the browser :) (yeah, they actually have a function for it!)

Well, it appears that the popup have few divs and iframe behind it to make it look and work so cool (great job on that Microsoft!), I figured the problem was that the height was not calculating correctly on the second call to autoSize. It disregards the maximum size of the parent window, and does not reposition the window vertically.

I had to do it myself.

I came up with this code, now – it is not fail proof, and might not work on some scenarios – but it did work for me, our QA and our customers. I will appreciate if you have any comments or fixes to this code, or if you know of another more “clean” solution to this issue.

//call autoSize
window.frameElement.autoSize();
//Fix scroll bar and positioning. $kw is our alias for jQuery
var bodyHeight = $kw(top.document.body).height();
var dialogMaxHeight = bodyHeight - 60;
//if dialog is too high
if ($kw(window.frameElement).height() > dialogMaxHeight) {
//move dialog to top
top.document.getElementsByClassName('ms-dlgContent')[0].style.top = '8px';
top.document.getElementsByClassName('ms-dlgContent')[0].previousSibling.style.top = '8px';
//resize all dialog divs/frames/whatnot
window.frameElement.style.height = dialogMaxHeight + 'px';
top.document.getElementsByClassName('ms-dlgContent')[0].previousSibling.style.height = dialogMaxHeight + 'px';
top.document.getElementsByClassName('ms-dlgContent')[0].style.height = dialogMaxHeight + 32 + 'px';
top.document.getElementsByClassName('ms-dlgBorder')[0].style.height = dialogMaxHeight + 32 + 'px';
}
//end issue fix




Thanks, Shai.

Thursday, November 24, 2011

Redirect to custom error page from event handler

I’ve seen a lot of posts saying you can’t redirect to a custom error page from a SPItemEventReceiver in SharePoint 2007.

In 2010, you can use Properties.RedirectUrl which will send you to a custom error page, instead of the OOB error page that displays your properties.ErrorMessage. this is simple enough, all you have to do is set properties.Cancel = true – and the custom redirect Url kicks in.

But still, in 2007 this property does not exist, and in 2010 – you might want to have a custom redirect page and not cancel the event.

Good news – it is all possible!

Redirecting

The way to do it, is to add some code to a sync event. Async events can’t help you since they are not guaranteed to run immediately, and they don’t have any user context when they execute…

You can use the SPUtility.Redirect(PageUrl, SPRedirectFlags, HTTPContext); within your ItemAdding, ItemUpdating or ItemDeleting event handler to redirect your user to a custom page.

This redirection kicks in and throws the “ThreadAbortException”, stopping the rest of your code from running and redirecting the user to your custom page.

Few things to know:

1. ThreadAbortException is a “catch me if you can” exception. you can put it in try…catch… block, but this exception will always bubble up and be thrown again and again. essentially, only the catch and finally code blocks will run after this exception is thrown. you can set “SPRedirectFlags.DoNotEndResponse” if you don’t want this exception to be thrown and it should still redirect at the end, but using SPRedirectFlags.Default will throw it.

2. You may notice, that if you try using HTTPContext.Current in your event handler method – you will get a null context. So, the only way to get the HTTPContext.Current is in your event handler class constructor. save it as a member, and use it in your event handlers. DONT SAVE IT AS A STATIC MEMBER! Use any instance member (private/public/protected…) but don’t use a static member as you may run into unexpected trouble.

Cancelling the event

Now, we took care of redirecting, but it is good to remember that this on its own will not cancel the item event and it will still be updated/added/deleted.

If you want to cancel the item event, you still have to do that using properties.Cancel = true.

Since we talked about ThreadAbortException – now you know you have to put the SPUtility.Redirect code in a try block, and set the properties.Cancel in the catch block.

Code Example:

Your code should look like this:

try
{
SPUtility.Redirect("/_layouts/KWizCom/AppError.aspx?ErMsg=" + errMessage, SPRedirectFlags.DoNotEndResponse, currentContext);
}
catch
{
properties.ErrorMessage = errMessage;
properties.Cancel = true;
}

Good luck!