AJAX browser history - two issues

  • Thread starter Thread starter Pete Hurst
  • Start date Start date
P

Pete Hurst

[Possibly duplicate - something went wrong due to Windows Live ID (I think)
and my first attempt got semi-lost. It appears on the web, but with a date 3
days in the future, and is not showing at all in Windows Mail. I am now
avoiding Windows Live ID...]

I've been setting up AJAX browser history on a major project, and it's
generally working very nicely. I've run into two specific problems however
which are giving me real headaches;

1. Strange double-postback.

If any state strings contain % characters (i.e. URL-encoded values) then a
double-postback is triggered for absolutely no reason. Specifically I'm
storing a path in the browser state, so the forward slashes get encoded, and
the page performs *two* partial updates for no obvious reason. To work
around this I'm substituting the / for an _ and then I get a single postback
as expected. But other values I'm storing in the state might occasionally
have URL-encodable characters and there's no way I can prevent this, or know
what characters might be there, so is this a known problem and is there a
fix or better workaround? (For instance, I am storing the text of a search
query in the browser state. This is user-inputted so could contain
anything!)

2. ScriptManager.IsInAsyncPostback is false when it shouldn't be.

I think this is a page lifecycle issue. In response to a button click by the
user, I am attempting to set a browser history point. This causes an
InvalidOperationException "A history point can only be created during an
asynchronous postback." - even when I have already called the Update method
on one of my UpdatePanels.

I'm loading all my controls very early in the page lifecycle, to ensure that
a number of dynamically created controls work properly. So it seems that
when the button click is fired at this stage, the ScriptManager is not yet
aware that it is performing an AJAX update. Later on in the page lifecycle
I've verified that IsInAsyncPostback becomes true. Is there any further
information on exactly when it's safe to add history points?

Thanks,

Pete Hurst
 
Hi Pete,

Quote from Pete==================================================
1. Strange double-postback.

If any state strings contain % characters (i.e. URL-encoded values) then a
double-postback is triggered for absolutely no reason. Specifically I'm
storing a path in the browser state, so the forward slashes get encoded, and
the page performs *two* partial updates for no obvious reason. To work
around this I'm substituting the / for an _ and then I get a single postback
as expected. But other values I'm storing in the state might occasionally
have URL-encodable characters and there's no way I can prevent this, or know
what characters might be there, so is this a known problem and is there a
fix or better workaround? (For instance, I am storing the text of a search
query in the browser state. This is user-inputted so could contain
anything!)

==================================================

From your description a simple workaround is to replace strings like "%"
that may potentially cause double postback with some safe strings (or do a
URL decoding).

To figure out the root cause, I did a simple test but could not reproduce
this issue. My test code is like below:

protected void GridView1_PageIndexChanged(object sender, EventArgs e)
{
if (ScriptManager1.IsInAsyncPostBack &&
!ScriptManager1.IsNavigating) {
ScriptManager1.AddHistoryPoint("key", @"/" +
this.GridView1.PageIndex.ToString(), "Page " +
this.GridView1.PageIndex.ToString()); //also tried "%"
}
}

protected void ScriptManager1_Navigate(object sender,
HistoryEventArgs e)
{
var s= e.State["key"];
if (string.IsNullOrEmpty(s))
{
this.GridView1.PageIndex = 0;
}
else
{
s = s.Substring(1);
this.GridView1.PageIndex=Convert.ToInt32(s);
}
}


Quote from Pete==================================================
2. ScriptManager.IsInAsyncPostback is false when it shouldn't be.
==================================================

I cannot repro it either. It would be better if you could send me a demo
project that can reproduce the above two issues. I'll test it on my side to
see what the problem is. My email is (e-mail address removed). Please update
here after sending the project in case I missed that email.


Regards,
Allen Chen
Microsoft Online Support

Delighting our customers is our #1 priority. We welcome your comments and
suggestions about how we can improve the support we provide to you. Please
feel free to let my manager know what you think of the level of service
provided. You can send feedback directly to my manager at:
(e-mail address removed).

==================================================
Get notification to my posts through email? Please refer to
http://msdn.microsoft.com/en-us/subscriptions/aa948868.aspx#notifications.

Note: MSDN Managed Newsgroup support offering is for non-urgent issues
where an initial response from the community or a Microsoft Support
Engineer within 2 business day is acceptable. Please note that each follow
up response may take approximately 2 business days as the support
professional working with you may need further investigation to reach the
most efficient resolution. The offering is not appropriate for situations
that require urgent, real-time or phone-based interactions. Issues of this
nature are best handled working with a dedicated Microsoft Support Engineer
by contacting Microsoft Customer Support Services (CSS) at
http://msdn.microsoft.com/en-us/subscriptions/aa948874.aspx
==================================================
This posting is provided "AS IS" with no warranties, and confers no rights.
 
Quote from Pete==================================================
1. Strange double-postback.
[snip]

From debugging in Firebug I've noticed a couple of extra things;

[in MicrosoftAjax.debug.js:]

If I breakpoint in Sys$_Application$_navigate (line 4344), I can see it's
getting called prior to the 2nd postback being initiated.

In the Stack I can see it's called directly from _onIdle:

function Sys$_Application$_onIdle() {
delete this._timerCookie;

var entry = this.get_stateString();
if (entry !== this._currentEntry) {
if (!this._ignoreTimer) {
this._historyPointIsNew = false;
this._navigate(entry);
this._historyLength = window.history.length;
}
}
else {
this._ignoreTimer = false;
}
this._timerCookie = window.setTimeout(this._timerHandler, 100);
}

At this point, get_stateString() returns "&&test=test/test" whereas
_currentEntry == "&&test=test%2ftest". So, at this stage there is a mismatch
between the two state values. So (entry !== this._currentEntry) should be
false but it's returning true. _ignoreTimer is also false.

In Sys$_Application$_onPageRequestManagerEndRequest:

var entry = this._serializeState(this._state);
if (entry !== this._currentEntry) {
this._ignoreTimer = true;
this._setState(entry);
this._raiseNavigate();
}

So this makes a call to Sys$_Application$_setState .

In this function, _ignoreTimer is set to false *before the
window.location.hash has been updated*.

Additionally I noticed that on that line:
window.location.hash = entry;

Firefox appears to automatically decode the URL at it is stored into
window.location.hash. So entry=="@@test=test%2ftest" whereas
window.location.hash == "#@@test=test/test"

Finally - Sys$_Application$get_stateString returns its value derived from
windows.location.hash, whereas _currentEntry has been populated with the
original *encoded* value. This is where the mismatch is creeping in.

I think you can fix the issue in Sys$_Application$_setState by changing:

var currentHash = this.get_stateString();
this._currentEntry = entry;

To:

var currentHash = this.get_stateString();
this._currentEntry = currentHash;

This would ensure that you are testing against the decoded hash, and the
_ignoreTimer flag is less relevant; although it looks to me like there could
be marginal issues where _onIdle gets called at slightly the wrong time.

Hope that sheds some light!

Pete Hurst
 
Quote from Pete==================================================
I couldn't reproduce the 2nd issue yet, and the main project where I'm
having trouble is just too huge to send you. I'll keep looking at it.

I've fixed the 2nd issue now, the problem was simply that my button wasn't
inside an UpdatePanel, therefore wasn't causing an async postback -- my bad,
sorry!

Pete
 
Hi Pete,
I've fixed the 2nd issue now, the problem was simply that my button wasn't
inside an UpdatePanel, therefore wasn't causing an async postback -- my bad,
sorry!

Thanks for your reply. For the first issue, I can reproduce it on my side.
A workaround is to set EnableSecureHistoryState="true" for the
ScriptManager.

Could you test it to see if it can solve the problem?


Regards,
Allen Chen
Microsoft Online Support
 
Allen said:
Thanks for your reply. For the first issue, I can reproduce it on my side.
A workaround is to set EnableSecureHistoryState="true" for the
ScriptManager.

That definitely works around it. Interestingly, the encrypted hash is of the
form:
#&&/wEXAwUBcQUBJwUB [...]

As you can see this introduces an intial / into the string but this time it
doesn't trigger the issue.

Well, it's a shame to lose the friendlier URLs I had in the address bar. The
Sys$_Application$_setState change I previously suggested didn't work, but I
modified it and now have the issue fixed. By still removing
this._currentEntry = entry, but adding this._currentEntry =
this.get_stateString() just near the end of _setState, it appears I have
everything working.

The fixed version is as follows:

function Sys$_Application$_setState(entry, title) {
entry = entry || '';
if (entry !== this._currentEntry) {
if (window.theForm) {
var action = window.theForm.action;
var hashIndex = action.indexOf('#');
window.theForm.action = ((hashIndex !== -1) ?
action.substring(0, hashIndex) : action) + '#' + entry;
}

if (this._historyFrame && this._historyPointIsNew) {
this._ignoreIFrame = true;
this._historyPointIsNew = false;
var frameDoc = this._historyFrame.contentWindow.document;
frameDoc.open("javascript:'<html></html>'");
frameDoc.write("<html><head><title>" + (title ||
document.title) +
"</title><scri" + "pt
type=\"text/javascript\">parent.Sys.Application._onIFrameLoad('" +
entry + "');</scri" + "pt></head><body></body></html>");
frameDoc.close();
}
// _ignoreTimer line moved
var currentHash = this.get_stateString();
// Line removed from here
if (entry !== currentHash) {
var loc = document.location;
if (loc.href.length - loc.hash.length + entry.length > 1024)
{
throw
Error.invalidOperation(Sys.Res.urlMustBeLessThan1024chars);
}
if (this._isSafari2()) {
var history = this._getHistory();
history[window.history.length -
this._historyInitialLength + 1] = entry;
this._setHistory(history);
this._historyLength = window.history.length + 1;
var form = document.createElement('form');
form.method = 'get';
form.action = '#' + entry;
document.appendChild(form);
form.submit();
document.removeChild(form);
}
else {
window.location.hash = entry;
}
if ((typeof (title) !== 'undefined') && (title !== null)) {
document.title = title;
}
}
// This is the inserted line:
this._currentEntry = this.get_stateString();
// Moved this down here, just to make sure!
this._ignoreTimer = false;
}
}


I'm not sure on the licensing of MicrosoftAjax.js, am I allowed to deploy my
modified version?

Finally, through this testing I've found an additional bug which is
happening server-side. If you attempt to put a single quote ( ' ) in a state
value, the AJAX request never completes, with a JSON validation error being
reported. Obviously single quotes require escaping when emitted in a JSON
string. I was able to work around this with:

String.Replace("'","%27") // Prior to calling
ScriptManager.AddHistoryPoint
String.Replace("%27","'") // When accessing state from
ScriptManager.Navigate event

(Single quotes normally aren't encoded as they are perfectly valid in URLs.)

Pete Hurst
 
Hi Pete,
I'm not sure on the licensing of MicrosoftAjax.js, am I allowed to deploy my
modified version?

Licensing issue is out of MSDN Newsgroup Support Boundary:
http://blogs.msdn.com/msdnts/archive/2006/11/08/msdn-service-introduction.as
px

To get the answer first you can check out Microsoft Permissive License
(Ms-PL):
http://msdn.microsoft.com/en-us/asp.net/dd162267.aspx

Quote:

(G) If you make any additions or changes to the original software, you may
only distribute them under a new namespace. In addition, you will clearly
identify your changes or additions as your own.

As to how shall you do with JS files you can ask in AJAX team blog:

http://weblogs.asp.net/atlas-team/default.aspx

If you attempt to put a single quote ( ' ) in a state
value, the AJAX request never completes, with a JSON validation error being
reported.

As to this issue, could you tell me how to reproduce it with the project
you provided? I cannot reproduce it.


Regards,
Allen Chen
Microsoft Online Support
 
Allen said:
(G) If you make any additions or changes to the original software, you may
only distribute them under a new namespace. In addition, you will clearly
identify your changes or additions as your own.

Hmm, not quite sure how to change the namespace of the whole of
Sys.Application :) I'll take your advice and ask around...
As to this issue, could you tell me how to reproduce it with the project
you provided? I cannot reproduce it.

Just type a single apostrophe into the textbox and click Send. The history
state fails to update in the address bar, after the postback has been
completed.

It's reproducable in IE as well (IE8 here), and the exception is more
obvious. It shows at random times or not at all in Firefox. Error as
follows:

===
Webpage error details

User Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0;
SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.21022; .NET CLR
3.5.30729; .NET CLR 3.0.30618; InfoPath.2; Tablet PC 2.0)
Timestamp: Thu, 23 Apr 2009 10:55:06 UTC


Message: Sys.ArgumentException: Cannot deserialize. The data does not
correspond to valid JSON.
Parameter name: data
Line: 4723
Char: 21
Code: 0
URI:
http://localhost:54859/BrowserHisto...0z3YBkReZMHSWOIYwKBvbxJhlmMCzJ86c1&t=67f0a167


Pete
 
Hi Pete,
Just type a single apostrophe into the textbox and click Send.

Thanks for your reply. I can reproduce it. I can eliminate the error by
setting EnableSecureHistoryState="true" as well.

It looks like setting EnableSecureHistoryState to true will bring some
limitations. To know if it's by design or not you can submit a feedback on
the connect site:

https://connect.microsoft.com/VisualStudio

or ask the question in AJAX team blog to contact our project team engineer
directly to report this issue. Your feedback is really apprecated.

Regards,
Allen Chen
Microsoft Online Support
 
Allen said:
It looks like setting EnableSecureHistoryState to true will bring some
limitations. To know if it's by design or not you can submit a feedback on
the connect site:

Should read "EnableSecureHistoryState to false" but yes, seems that way. I
hope its *not* by design, since I've been able to fix both issues relatively
easily, it would be fairly unimpressive if the problem had been known about
but not resolved or documented!

Thanks for helping me work through this anyway, I'll pass my findings on as
you suggest...

Regards

Pete
 
Allen said:
It looks like setting EnableSecureHistoryState to true will bring some
limitations. To know if it's by design or not you can submit a feedback on
the connect site:

https://connect.microsoft.com/VisualStudio

or ask the question in AJAX team blog to contact our project team engineer
directly to report this issue. Your feedback is really apprecated.

Submitted bug report:
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=437199

I couldn't find a specific AJAX team blog. There's an "AJAX team blogs" page
in ASP.NET blogs, but it links off to various blogs and its not clear which
of them I could post to. So I hope the bug report is enough!

Thanks again

Pete
 
Back
Top