Tuesday, July 13, 2010

Internal mechanism of showing indicator of long operations in Sharepoint using SPLongOperation

In this post I’m going to show how implemented indicator of long operations in Sharepoint internally. Most of posts about SPLongOperation are limited by examples of using it. I will go further and show how it is implemented internally. So lets start.

If you worked with Sharepoint then you will probably saw OTB indicator of long operations:

image

This indicator also can be used in your custom code, e.g. in handler of button OnClick event or any other place. In order to show it you need to use SPLongOperation class (defined in Microsoft.SharePoint.dll). The usage is quite simple: all you need is to create instance of SPLongOperation, call Begin method at the beginning of operation and End method on the end. Also you can specify your own title and description for long operation.

For example I created simple application _layouts page:

   1: <%@ Page Language="C#" %>
   2: <%@ Import Namespace="Microsoft.SharePoint" %>
   3: <%@ Import Namespace="System.Threading" %>
   4:  
   5: <html xmlns="http://www.w3.org/1999/xhtml" >
   6: <head>
   7:     <title>Long operation example</title>
   8: </head>
   9: <body>
  10:     <form id="form1" runat="server">
  11:     <script runat="server">
   1:  
   2:         protected void btn_OnClick(object sender, EventArgs e)
   3:         {
   4:             using (var operation = new SPLongOperation(this))
   5:             {
   6:                 operation.LeadingHTML = "My long operation";
   7:                 operation.TrailingHTML = "Description of long operation";
   8:             
   9:                 operation.Begin();
  10:                 
  11:                 Thread.Sleep(5000); // simulate long operation
  12:                 
  13:                 operation.End(this.Request.RawUrl);
  14:             }
  15:         }        
  16:     
</script>
  12:  
  13:     <asp:Button ID="btn" runat="server" Text="Click" OnClick="btn_OnClick" />
  14:  
  15:     </div>
  16:   </form>
  17: </body>
  18: </html>
  19:  

There is a button on the page. When user clicks this button the following indicator of long operation will be shown:

image

After 5 seconds indicator is hided and page is shown again. Note that operation will be executed synchronously so if it will take more time than request timeout specified in your web application you will get Request timeout exception. And you will understand why it happens when see the rest of this article.

Most of posts which tell about SPLongOperation are finished in this place. Lets go further and see under the hood. What happens when you call SPLongOperation.Begin() method? Lets see this method using reflector:

   1: public void Begin()
   2: {
   3:     string s = GearFileContent.Replace(
   4: "<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>",
   5: Thread.CurrentThread.CurrentUICulture.LCID.ToString(CultureInfo.InvariantCulture));
   6:     this.m_srGearAspx = new StringReader(s);
   7:     this.WriteGearToSearchString(m_strBeginContent);
   8:     this.m_Page.Response.Write("<div id=GearPage>");
   9:     this.WriteGearToSearchString(m_strEndContent);
  10:     this.m_Page.Response.Write("</div>");
  11:     this.WriteGearToSearchString(m_strTargetDots);
  12: }

It reads content of gear.aspx page which is located in 12/template/layouts folder on file system. Then it reads content line by line and replaces placeholders by real values. At first it replaces System.Threading.Thread.CurrentThread.CurrentUICulture.LCID placeholder by real integer value of current locale. So css path from

   1: <link rel="stylesheet" type="text/css"
   2: href="/_layouts/<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>/styles/core.css" />

will be:

   1: <link rel="stylesheet" type="text/css" href="/_layouts/1033/styles/core.css" />

if you use English locale (lcid = 1033). Then it replaces resources placeholders, so strings like:

   1: <HTML dir="&lt;SharePoint:EncodedLiteral runat='server'
   2: text='<%$Resources:wss,multipages_direction_dir_value%>' EncodeMethod='HtmlEncode'/>">

will be expanded to:

   1: <HTML dir="ltr">

I.e. code retrieves multipages_direction_dir_value resource object from wss.resx file and replaces it in output html.

If you will see inside gear.aspx file you will find several other placeholders in layout:

   1: <html>
   2:         <head>
   3:                 <title>
   4:                     ...
   5:                 </title>
   6:                 ...
   7:                 <script language="javascript">
   1:  
   2:                    function gotoNextPage() { }
   3:                 
</script>
   8:         </head>
   9:         <body onload="javascript:gotoNextPage()">
  10:         SPLongOperation.BeginContent
  11:             ...
  12:             <!-- LEADING HTML -->
  13:             </span><span class='ms-descriptiontext'>
  14:             <!-- TRAILING HTML -->
  15:             ...
  16:         SPLongOperation.EndContent
  17:         SPLongOperation.Dots
  18:         <script language="javascript">
  19:         function gotoNextPage()
  20:         {
  21:             window.location.replace("SPLongOperation.RedirectUrl");
  22:         }
  23:         </body>
  24: </html>

Placeholders SPLongOperation.BeginContent, SPLongOperation.EndContent, SPLongOperation.Dots are replaced by empty string. Placeholders <!-- LEADING HTML –> and <!-- TRAILING HTML –> are replaced by LeadingHTML and TrailingHTML properties of SPLongOperation object.

Now very important moment: in the Begin() method SPLongOperation writes response until SPLongOperation.Dots placeholder (see code above). So html will NOT contain the following lines:

   1: <script language="javascript">
   2: function gotoNextPage()
   3: {
   4:     window.location.replace("SPLongOperation.RedirectUrl");
   5: }
   6: </body>

You can check it by yourself if you will click View Source when long operation indicator will be shown. See that at the top of gear.aspx page there is another javascript function with the same name gotoNextPage but with empty body:

   1: <html>
   2:         <head>
   3:                 <title>
   4:                     ...
   5:                 </title>
   6:                 ...
   7:                 <script language="javascript">
   1:  
   2:                    function gotoNextPage() { }
   3:                 
</script>
   8:         </head>
   9:         <body onload="javascript:gotoNextPage()">
  10:         ...

And this function is assigned to onload event of body element. It means that until another function with non-empty body will be written to the response, first loaded function with empty body will be used (actually this 1st function is not called at all because body element is not fully loaded). And as you probably already guess SPLongOperation.End(…) method replaces SPLongOperation.RedirectUrl placeholder by redirect URL (in example above this is the same page as was initially requested) and writes the rest of file to the response:

   1: window.location.replace("SPLongOperation.RedirectUrl");

is replaced by

   1: window.location.replace("/_layouts/test.aspx");

(I assumed that URL of testing page is test.aspx). Unfortunately SPLongOperation.End(…) method is obfuscated but I found one single post here which shows implementation of this method:

   1: public void End(string strProposedRedirect, SPRedirectFlags rgfRedirect,
   2: HttpContext context, string queryString)
   3: {
   4:     string str;
   5:     if (!this.m_bLongOperationEnded)
   6:     {
   7:         this.m_bLongOperationEnded = true;
   8:         lock (this.lockObject)
   9:         {
  10:             this.m_bLongOperationStarted = false;
  11:             if (this.m_Timer != null)
  12:             {
  13:                 this.m_Timer.Dispose();
  14:                 this.m_Timer = null;
  15:             }
  16:             if (this.m_ichLine > -1)
  17:             {
  18:                 this.WriteGearRestOfLine(m_strTargetDots);
  19:             }
  20:             if (!SPUtility.DetermineRedirectUrl(strProposedRedirect,
  21: rgfRedirect, context, queryString, out str))
  22:             {
  23:                 str = strProposedRedirect;
  24:             }
  25:         }
  26:     }
  27:     else
  28:     {
  29:         return;
  30:     }
  31:     this.WriteGearToSearchString(m_strTargetRedir);
  32:     if (this.m_ichLine > -1)
  33:     {
  34:         string s = SPHttpUtility.EcmaScriptStringLiteralEncode(str);
  35:         this.m_Page.Response.Write(s);
  36:         this.WriteGearRestOfLine(m_strTargetRedir);
  37:     }
  38:     this.WriteGearRemaining();
  39:     this.m_srGearAspx.Close();
  40:     this.m_srGearAspx = null;
  41:     this.m_Page.Response.Flush();
  42:     this.m_Page.Response.End();
  43: }

So it uses known feature of javascript which allows to override functions by defining function with the same name in later execution step (e.g. in the end of page). After that SPLongOperation.End(…) method calls Response.End() which in turn caused ThreadAbortException. So execution after SPLongOperation.End(…) is interrupted and response is given to client. After this body onload handler is raised and overridden gotoNextPage function is called which redirects you on the specified URL.

That how SPLongOperation is implemented internally. Hope it will help you to understand Sharepoint mechanisms more deeply.

2 comments:

  1. Excellent post. Good explanation. But my doubt is that how exactly it does everything in a single postback without going back and forth?

    ReplyDelete
  2. hi nlvraghavendra,
    yes - these actions are performed during single postback. That's why if long operation will take more time than specified in web.config (httpRuntime element > executionTimeout attribute), you will still get Request Timeout exception.

    ReplyDelete