Saturday, May 30, 2009

Problem with partial post back on Chrome and Safari using UpdateProgress control

We had the requirement of developing web site supporting cross browser. When we started development in late 2007, google did not launch Chrome at that time and we were developing ASP.Net 2.0 web site using AjaxControlToolKit. After user clicks on a button, we invoke UpdateProgress control and display “loading” gif. We made that as user control to dumped it in Master page. The user control was simple. We wrote few javascript and it was working as desired.
following is the code snippet;
<%@ Control Language="C#" AutoEventWireup="true" Codebehind="ProgressBar.ascx.cs"
Inherits="CVC.UX.UserControls.ProgressBar" %>
<%@ Register Assembly="System.Web.Extensions" Namespace="System.Web.UI" TagPrefix="asp" %>
<%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="ajaxtoolkit" %>
<style type="text/css">
.modalBackground 
{ 
background-color: Gray; 
filter: alpha(opacity=50); 
opacity: 0.50; 
}

</style>

<script type="text/javascript" language="javascript"> 

var ModalProgress ='<%= ModalProgress.ClientID %>';         

</script>

<script type="text/javascript" language="javascript">
Sys.WebForms.PageRequestManager.getInstance().add_beginRequest(beginReq); 
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endReq);    

//Create buffer to be used for JAWS - 508 compliance.
//prepareBuffer();

function beginReq(sender, args){ 

// shows the Popup 
$find(ModalProgress).show();        
} 

function endReq(sender, args) 
{ 
//  shows the Popup   
$find(ModalProgress).hide(); 

//updateBuffer();
} 

function prepareBuffer()
{
var objNew = document.createElement('p');
var objHidden = document.createElement('input');

objHidden.setAttribute('type', 'hidden');
objHidden.setAttribute('value', '1');
objHidden.setAttribute('id', 'virtualbufferupdate');
objHidden.setAttribute('name', 'virtualbufferupdate');

objNew.appendChild(objHidden);
document.body.appendChild(objNew);
}

function updateBuffer()
{
var objHidden = document.getElementById('virtualbufferupdate');

if (objHidden)
{
if (objHidden.getAttribute('value') == '1')
objHidden.setAttribute('value', '0');
else
objHidden.setAttribute('value', '1');
}
}
</script>

<asp:Panel ID="panelUpdateProgress" runat="server" CssClass="updateProgress">
<asp:UpdateProgress ID="UpdateProg1" DisplayAfter="0" runat="server">
<ProgressTemplate>
<asp:Image runat="server" ID="imgLoading" ImageUrl="~/Images/loading.gif" />
</ProgressTemplate>
</asp:UpdateProgress>
</asp:Panel>
<ajaxtoolkit:ModalPopupExtender ID="ModalProgress" runat="server" TargetControlID="panelUpdateProgress" RepositionMode="RepositionOnWindowResizeAndScroll"
BackgroundCssClass="modalBackground" PopupControlID="panelUpdateProgress" />


When chrome was launched, our website was behaving very weirdly in partial postbacks. So if you click on a button and it reloads the same page with extra content then the ProgressBar control will go haywired.
Apparently, the “endReq” function will never get invoked and progress bar will not disappear and even though the page is loaded correctly. User will have the shut down the browser in order to resume their work. It was very frustrating. We knew that its not our code, its because of Chrome, some piece of code is behaving abnormally.
After some research, we realized that ajax control toolkit has a logic to go through if condition and check each browser and then set some essential information on their Sys object.

Here’s the code snippet of the Javascript code which comes with Ajaxtoolkit:

Sys.Browser = {};

Sys.Browser.InternetExplorer = {};
Sys.Browser.Firefox = {};
Sys.Browser.WebKit = {};
Sys.Browser.Safari = {};
Sys.Browser.Opera = {};

Sys.Browser.agent = null;
Sys.Browser.hasDebuggerStatement = false;
Sys.Browser.name = navigator.appName;
Sys.Browser.version = parseFloat(navigator.appVersion);

if (navigator.userAgent.indexOf(' MSIE ') > -1) {
Sys.Browser.agent = Sys.Browser.InternetExplorer;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/MSIE (\d+\.\d+)/)[1]);
Sys.Browser.hasDebuggerStatement = true;
}
else if (navigator.userAgent.indexOf(' Firefox/') > -1) {
Sys.Browser.agent = Sys.Browser.Firefox;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/ Firefox\/(\d+\.\d+)/)[1]);
Sys.Browser.name = 'Firefox';
Sys.Browser.hasDebuggerStatement = true;
}
else if (navigator.userAgent.indexOf(' Safari/') > -1) {
Sys.Browser.agent = Sys.Browser.Safari;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/ Safari\/(\d+\.\d+)/)[1]);
Sys.Browser.name = 'Safari';
}
else if (navigator.userAgent.indexOf('Opera/') > -1) {
Sys.Browser.agent = Sys.Browser.Opera;
}

Safari and Chrome both the browser uses WebKit and when these browsers are rendered, it reads as WebKit, instead of Safari or Chrome.
So now we know the problem, we cannot make changes to the script files that Ajax toolkit uses, so how do we fix the problem?

We created a new file and called it as “WebKitHack.js” and pasted the following code:

Sys.Browser = {};

Sys.Browser.InternetExplorer = {};
Sys.Browser.Firefox = {};
Sys.Browser.WebKit = {};
Sys.Browser.Safari = {};
Sys.Browser.Opera = {};

Sys.Browser.agent = null;
Sys.Browser.hasDebuggerStatement = false;
Sys.Browser.name = navigator.appName;
Sys.Browser.version = parseFloat(navigator.appVersion);

if (navigator.userAgent.indexOf(' MSIE ') > -1) {
Sys.Browser.agent = Sys.Browser.InternetExplorer;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/MSIE (\d+\.\d+)/)[1]);
Sys.Browser.hasDebuggerStatement = true;
}
else if (navigator.userAgent.indexOf(' Firefox/') > -1) {
Sys.Browser.agent = Sys.Browser.Firefox;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/ Firefox\/(\d+\.\d+)/)[1]);
Sys.Browser.name = 'Firefox';
Sys.Browser.hasDebuggerStatement = true;
}
else if (navigator.userAgent.indexOf('WebKit/') > -1) {
Sys.Browser.agent = Sys.Browser.WebKit;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/WebKit\/(\d+(\.\d+)?)/)[1]);
Sys.Browser.name = 'WebKit';
}
else if (navigator.userAgent.indexOf(' Safari/') > -1) {
Sys.Browser.agent = Sys.Browser.Safari;
Sys.Browser.version = parseFloat(navigator.userAgent.match(/ Safari\/(\d+\.\d+)/)[1]);
Sys.Browser.name = 'Safari';
}
else if (navigator.userAgent.indexOf('Opera/') > -1) {
Sys.Browser.agent = Sys.Browser.Opera;
}

The only change that i made was to add an extra condition to check for WebKit.

Now we need a way to override Ajaxcontrol toolkit’s code with our code, so in our master file we added the following line of code and Bingo!!.
Partial post back was working as expected in Safari and Chrome.
<asp:UpdatePanel runat="server" ID="mainUpdPan">
<contenttemplate>
<asp:ScriptManagerProxy ID="ScriptManagerProxy1" runat="server">
<Scripts>
<asp:ScriptReference Path="Javascript/SafariHack.js" />
</Scripts>
</asp:ScriptManagerProxy>
</contenttemplate>
</asp:UpdatePanel>