Wednesday, December 11, 2013

Problem with only first postback working in Chrome from UpdatePanel in Sharepoint site

Some time ago I investigated interesting problem: in existing site running on Sharepoint 2010 on the page with UpdatePanel only first postback worked in Chrome (when possible I try to avoid using of UpdatePanel, preferring jquery.ajax() instead. But in this case site was implemented quite long time ago and we couldn’t change it). In other browsers postbacks worked and UpdatePanel was modified successfully. What was even more unclear is that even in Chrome postbacks started to work after removing of iframes with google maps code (going forward I must say that google maps is not directly related with the problem). So I started analysis with developer tools in Chrome, comparing it with results in firebug in FF.

There were no any errors neither in Chrome console nor in javascript debugger. The first thing I did was setting ScriptMode property of ScriptManager control to Debug. It made all scripts (including those which are added via ScriptResource.axd handler) user friendly (not minified as they were before), which made debugging possible.

Debugging showed that problem comes from Sys$WebForms$PageRequestManager$_onFormSubmit() method from MicrosoftAjaxWebForms.js file:

   1:   function Sys$WebForms$PageRequestManager$_onFormSubmit(evt) {
   2:       var continueSubmit = true;
   3:       var isCrossPost = this._isCrossPost;
   4:       this._isCrossPost = false;
   5:       var i, l;
   6:       if (this._onsubmit) {
   7:           continueSubmit = this._onsubmit();
   8:       }
   9:       if (continueSubmit) {
  10:           for (i = 0, l = this._onSubmitStatements.length; i < l; i++) {
  11:               if (!this._onSubmitStatements[i]()) {
  12:                   continueSubmit = false;
  13:                   break;
  14:               }
  15:           }
  16:       }
  17:       if (!continueSubmit) {
  18:           if (evt) {
  19:               evt.preventDefault();
  20:           }
  21:           return;
  22:       }
  23:       var form = this._form;
  24:       if (isCrossPost) {
  25:           return;
  26:       }
  27:       if (this._activeDefaultButton && !this._activeDefaultButtonClicked) {
  28:           this._onFormElementActive(this._activeDefaultButton, 0, 0);
  29:       }
  30:       if (!this._postBackSettings.async) {
  31:           return;
  32:       }
  33:       var formBody = new Sys.StringBuilder();
  34:       formBody.append(encodeURIComponent(this._scriptManagerID) + '=' +
  35:          encodeURIComponent(this._postBackSettings.panelID) + '&');
  36:       var count = form.elements.length;
  37:       for (i = 0; i < count; i++) {
  38:           var element = form.elements[i];
  39:           var name = element.name;
  40:           if (typeof (name) === "undefined" || (name === null) || (name.length === 0) ||
  41:              (name === this._scriptManagerID)) {
  42:               continue;
  43:           }
  44:           var tagName = element.tagName.toUpperCase();
  45:           if (tagName === 'INPUT') {
  46:               var type = element.type;
  47:               if ((type === 'text') || (type === 'password') || (type === 'hidden') || (((type === 'checkbox') ||
  48:                   (type === 'radio')) && element.checked)) {
  49:                   formBody.append(encodeURIComponent(name));
  50:                   formBody.append('=');
  51:                   formBody.append(encodeURIComponent(element.value));
  52:                   formBody.append('&');
  53:               }
  54:           } else if (tagName === 'SELECT') {
  55:               var optionCount = element.options.length;
  56:               for (var j = 0; j < optionCount; j++) {
  57:                   var option = element.options[j];
  58:                   if (option.selected) {
  59:                       formBody.append(encodeURIComponent(name));
  60:                       formBody.append('=');
  61:                       formBody.append(encodeURIComponent(option.value));
  62:                       formBody.append('&');
  63:                   }
  64:               }
  65:           } else if (tagName === 'TEXTAREA') {
  66:               formBody.append(encodeURIComponent(name));
  67:               formBody.append('=');
  68:               formBody.append(encodeURIComponent(element.value));
  69:               formBody.append('&');
  70:           }
  71:       }
  72:       formBody.append("__ASYNCPOST=true&");
  73:       if (this._additionalInput) {
  74:           formBody.append(this._additionalInput);
  75:           this._additionalInput = null;
  76:       }
  77:       var request = new Sys.Net.WebRequest();
  78:       var action = form.action;
  79:       if (Sys.Browser.agent === Sys.Browser.InternetExplorer) {
  80:           var fragmentIndex = action.indexOf('#');
  81:           if (fragmentIndex !== -1) {
  82:               action = action.substr(0, fragmentIndex);
  83:           }
  84:           var queryIndex = action.indexOf('?');
  85:           if (queryIndex !== -1) {
  86:               var path = action.substr(0, queryIndex);
  87:               if (path.indexOf("%") === -1) {
  88:                   action = encodeURI(path) + action.substr(queryIndex);
  89:               }
  90:           } else if (action.indexOf("%") === -1) {
  91:               action = encodeURI(action);
  92:           }
  93:       }
  94:       request.set_url(action);
  95:       request.get_headers()['X-MicrosoftAjax'] = 'Delta=true';
  96:       request.get_headers()['Cache-Control'] = 'no-cache';
  97:       request.set_timeout(this._asyncPostBackTimeout);
  98:       request.add_completed(Function.createDelegate(this, this._onFormSubmitCompleted));
  99:       request.set_body(formBody.toString());
 100:       var eventArgs, handler = this._get_eventHandlerList().getHandler("initializeRequest");
 101:       if (handler) {
 102:           eventArgs = new Sys.WebForms.InitializeRequestEventArgs(request, this._postBackSettings.sourceElement);
 103:           handler(this, eventArgs);
 104:           continueSubmit = !eventArgs.get_cancel();
 105:       }
 106:       if (!continueSubmit) {
 107:           if (evt) {
 108:               evt.preventDefault();
 109:           }
 110:           return;
 111:       }
 112:       this._scrollPosition = this._getScrollPosition();
 113:       this.abortPostBack();
 114:       handler = this._get_eventHandlerList().getHandler("beginRequest");
 115:       if (handler) {
 116:           eventArgs = new Sys.WebForms.BeginRequestEventArgs(request, this._postBackSettings.sourceElement);
 117:           handler(this, eventArgs);
 118:       }
 119:       if (this._originalDoCallback) {
 120:           this._cancelPendingCallbacks();
 121:       }
 122:       this._request = request;
 123:       this._processingRequest = false;
 124:       request.invoke();
 125:       if (evt) {
 126:           evt.preventDefault();
 127:       }
 128:   }

This method makes actual POST request when some even triggers postback from inside UpdatePanel. 2 lines from this method are most interesting for us:

line 7: continueSubmit = this._onsubmit();
and
line 124: request.invoke();

On line 7 it checks should it make post request or not. If result of this._onsubmit() call will be false, on lines 17-22 method will exit without sending the request. In FF and IE it always returned true, while in Chrome – false. Let’s check first why it returned false in Chrome and then will analyze what happens when request is send on line 124.

Line 7 calls _spFormOnSubmitWrapper() function defined in init.js – standard Sharepoint javascript file located in /layouts/{lcid} folder. This file is one of the basic, without which Sharepoint sites won’t work properly. Here is the code of _spFormOnSubmitWrapper() function:

   1:  function _spFormOnSubmitWrapper() {
   2:      if (_spSuppressFormOnSubmitWrapper) {
   3:          return true;
   4:      }
   5:      if (_spFormOnSubmitCalled) {
   6:          return false;
   7:      }
   8:      if (typeof (_spFormOnSubmit) == "function") {
   9:          var retval = _spFormOnSubmit();
  10:          var testval = false;
  11:          if (typeof (retval) == typeof (testval) && retval == testval) {
  12:              return false;
  13:          }
  14:      }
  15:      _spFormOnSubmitCalled = true;
  16:      return true;
  17:  }

In Chrome global javascript variable _spFormOnSubmitCalled, defined in init.js file, was true, that’s why method _spFormOnSubmitWrapper() returned false (from lines 5-7). The question was why it was true?

In order to answer it we need to return to line 124 from the Sys$WebForms$PageRequestManager$_onFormSubmit() function, showed above. request.invoke() calls Sys$Net$_WebRequestManager$executeRequest() function from MicrosoftAjax.js:

   1:   function Sys$Net$_WebRequestManager$executeRequest(webRequest) {
   2:       var e = Function._validateParams(arguments, [{
   3:           name: "webRequest",
   4:           type: Sys.Net.WebRequest
   5:       }]);
   6:       if (e) throw e;
   7:       var executor = webRequest.get_executor();
   8:       if (!executor) {
   9:           var failed = false;
  10:           try {
  11:               var executorType = eval(this._defaultExecutorType);
  12:               executor = new executorType();
  13:           } catch (e) {
  14:               failed = true;
  15:           }
  16:           if (failed || !Sys.Net.WebRequestExecutor.isInstanceOfType(executor) ||
  17:               !executor) {
  18:               throw Error.argument("defaultExecutorType",
  19:                   String.format(Sys.Res.invalidExecutorType,
  20:                       this._defaultExecutorType));
  21:           }
  22:           webRequest.set_executor(executor);
  23:       }
  24:       if (executor.get_aborted()) {
  25:           return;
  26:       }
  27:       var evArgs = new Sys.Net.NetworkRequestEventArgs(webRequest);
  28:       var handler = this._get_eventHandlerList().getHandler("invokingRequest");
  29:       if (handler) {
  30:           handler(this, evArgs);
  31:       }
  32:       if (!evArgs.get_cancel()) {
  33:           executor.executeRequest();
  34:       }
  35:   }

In this method difference between FF and Chrome was in line 28 (var handler = this._get_eventHandlerList().getHandler("invokingRequest")). In FF it returned not empty function which set _spFormOnSubmitCalled global variable (because of which _spFormOnSubmitWrapper function returned false and because of which postbacks didn’t work in Chrome): _spResetFormOnSubmitCalledFlag() defined in init.js:

   1:  function _spResetFormOnSubmitCalledFlag( sender, e)
   2:  {
   3:  _spFormOnSubmitCalled=false;
   4:  }

So now the question was why _spResetFormOnSubmitCalledFlag() attached as a handler to "invokingRequest" event in FF and IE, but not in Chrome? Analysis of init.js showed that _spResetFormOnSubmitCalledFlag() function is attached to event in _spBodyOnLoadWrapper() function, which is also defined in init.js:

   1:  function _spBodyOnLoadWrapper() {
   2:      _spBodyOnLoadCalled = true;
   3:      if (!_spBodyOnPageShowRegistered && typeof (browseris) != "undefined" &&
   4:          !browseris.ie && typeof (window.addEventListener) == 'function') {
   5:          window.addEventListener('pageshow', _spBodyOnPageShow, false);
   6:          _spBodyOnPageShowRegistered = true;
   7:      }
   8:      if (typeof (Sys) != "undefined" && typeof (Sys.WebForms) != "undefined" &&
   9:          typeof (Sys.WebForms.PageRequestManager) != "undefined") {
  10:          var pageRequestMgr = Sys.WebForms.PageRequestManager.getInstance();
  11:          if (!_spPageLoadedRegistered && pageRequestMgr != null) {
  12:              pageRequestMgr.add_pageLoaded(_spPageLoaded);
  13:              _spPageLoadedRegistered = true;
  14:          }
  15:      }
  16:      if (!_spPageLoadedRegistered) {
  17:          _spPageLoaded();
  18:      }
  19:      _spFormOnSubmitCalled = false;
  20:      if (typeof (Sys) != "undefined" && typeof (Sys.Net) != "undefined" &&
  21:          typeof (Sys.Net.WebRequestManager) != "undefined") {
  22:  Sys.Net.WebRequestManager.add_invokingRequest(_spResetFormOnSubmitCalledFlag);
  23:      }
  24:      if (typeof (NotifyBodyLoadedAndExecuteWaitingJobs) != "undefined") {
  25:          NotifyBodyLoadedAndExecuteWaitingJobs();
  26:      }
  27:      ExecuteOrDelayUntilScriptLoaded(ProcessDefaultOnLoad, "core.js");
  28:      if (typeof (g_prefetch) == "undefined" || g_prefetch == 1) {
  29:          var prefetch = _spGetQueryParam("prefetch");
  30:          if (prefetch != 0) _spPreFetch();
  31:      }
  32:  }

And that’s was it: it turned out that problem with postbacks in Chrome was caused by known compatibility problem with Chrome in Sharepoint. In Chrome function _spBodyOnLoadWrapper() is not always loaded which causes many issues in Sharepoint. You will find a lot of examples of this problem if will search by function name and Sharepoint. This function supposed to be called by javascript embedded to the page. I.e. we have body element with onload handler:

   1:  <body onload="javascript: _spBodyOnLoadWrapper();">
   2:  </body>

If we will check the output html we will find that this handler is overridden by the following way in the end of page:

   1:  <script type="text/javascript" language="JavaScript">
   2:  // append an onload event handler
   3:  var S39A6E3E3__onload = document.body.onload;
   4:  if (typeof document.body.onload == 'function') {
   5:      document.body.onload = function() {
   6:          S39A6E3E3__onload();
   7:          document.getElementById('ctl00_PlaceHolderSearchArea_SmallSearchInputBox_S39A6E3E3_InputKeywords').name =
   8:  'InputKeywords';
   9:      }
  10:  } else {
  11:      document.body.onload = function() {
  12:          eval(S39A6E3E3__onload);
  13:          document.getElementById('ctl00_PlaceHolderSearchArea_SmallSearchInputBox_S39A6E3E3_InputKeywords').name =
  14:  'InputKeywords';
  15:      }
  16:  }
  17:   
  18:  function S39A6E3E3_OSBEK(event1) {
  19:      if ((event1.which == 10) || (event1.which == 13)) {
  20:          S39A6E3E3_Submit();
  21:          return false;
  22:      }
  23:  } {
  24:      var searchTextBox = document.getElementByI
  25:  ('ctl00_PlaceHolderSearchArea_SmallSearchInputBox_S39A6E3E3_InputKeywords');
  26:      if (searchTextBox.className.indexOf('s4-searchbox-QueryPrompt') == -1)
  27:  searchTextBox.className += searchTextBox.className ?
  28:  ' s4-searchbox-QueryPrompt' : 's4-searchbox-QueryPrompt';
  29:  } // -->
  30:  </script>

I.e. it stores original handler in S39A6E3E3__onload variable (line 3, name can be different on your site) and then attaches another handler, calls original handler inside it and makes additional actions (lines 5-8 or 11-14). But under some conditions neither of 2 overridden functions get called in Chrome. In our case it happened when page contained iframe with google maps. In one of the posts about similar problem I read assumption that it happens because of race condition in scripts and page loading in Chrome. Most probably the reason is really realted with scripts loading dependencies: in some cases when Chrome reaches the embedded javascript code shown above, _spBodyOnLoadWrapper() function is not attached to body.onload event, so neither of 2 ways of overriding cause calling of _spBodyOnLoadWrapper() function.

But how to fix this problem? The most frequent solution I saw used javascript timer and assumption that init.js will be loaded after predefined amount of time (e.g. 200 ms). I created more reliable solution using standard ExecuteOrDelayUntilScriptLoaded() function:

   1:  $(function () {
   2:      if (navigator && navigator.userAgent &&
   3:          /chrome/.test(navigator.userAgent.toLowerCase())) {
   4:          if (!_spBodyOnLoadCalled) {
   5:              ExecuteOrDelayUntilScriptLoaded(function () {
   6:                  if (!_spBodyOnLoadCalled) {
   7:                      _spBodyOnLoadWrapper();
   8:                  }
   9:              }, "init.js");
  10:          }
  11:      }
  12:  });

At first it checks that current browser is Chrome and then checks was _spBodyOnLoadWrapper() already called using another global variable _spBodyOnLoadCalled which is set to true in the beginning of _spBodyOnLoadWrapper(). Then it waits until init.js will be loaded using ExecuteOrDelayUntilScriptLoaded() function, checks _spBodyOnLoadCalled again and finally calls _spBodyOnLoadWrapper(). After that problem was fixed, i.e. postbacks from UpdatePanel started to work properly in Chrome.

This was interesting debugging session. Hope that it will be useful in your work.

6 comments:

  1. What is the MOSS version in you incident?
    Misrosoft released SP2 for MOSS2010 with enhanced chrome support.

    ReplyDelete
  2. hi Dmitry,
    On production it has 14.0.6029.1000 version, i.e. SP1. Probably this problem was fixed in SP2, because it was really critical problem which caused Sharepoint sites not working in Chrome.

    ReplyDelete
  3. I was facing the same issue and on the other servers it was working correctly. Really saved me!! thanx a ton!! very Helpful.

    ReplyDelete
  4. You are a genius, it works perfectly!! And incredible article ;)

    ReplyDelete
  5. thanks, but it is just a result of one deep analysis)

    ReplyDelete