AJAX browser history - two issues

Discussion in 'ASP .Net' started by Pete Hurst, Apr 20, 2009.

  1. Pete Hurst

    Pete Hurst Guest

    [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
     
    Pete Hurst, Apr 20, 2009
    #1
    1. Advertising

  2. 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 . 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:
    .

    ==================================================
    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.
     
    Allen Chen [MSFT], Apr 21, 2009
    #2
    1. Advertising

  3. Pete Hurst

    Pete Hurst Guest

    >> 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
     
    Pete Hurst, Apr 21, 2009
    #3
  4. Pete Hurst

    Pete Hurst Guest

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

    > 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
     
    Pete Hurst, Apr 21, 2009
    #4
  5. 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 Chen [MSFT], Apr 22, 2009
    #5
  6. Pete Hurst

    Pete Hurst Guest

    Allen wrote:

    > 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
     
    Pete Hurst, Apr 22, 2009
    #6
  7. 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 Chen [MSFT], Apr 23, 2009
    #7
  8. Pete Hurst

    Pete Hurst Guest

    Allen wrote:

    > (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...

    >>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.


    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
     
    Pete Hurst, Apr 23, 2009
    #8
  9. 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 Chen [MSFT], Apr 24, 2009
    #9
  10. Pete Hurst

    Pete Hurst Guest

    Allen wrote:

    > 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
     
    Pete Hurst, Apr 24, 2009
    #10
  11. Pete Hurst

    Pete Hurst Guest

    Allen Chen wrote:

    > 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
     
    Pete Hurst, May 1, 2009
    #11
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. minnie
    Replies:
    1
    Views:
    698
    Andrew Thompson
    Dec 13, 2006
  2. Sam Stephenson
    Replies:
    1
    Views:
    227
    Andrew Walrond
    Jun 18, 2005
  3. sa 125
    Replies:
    3
    Views:
    125
    Markus Fischer
    Jun 2, 2009
  4. Replies:
    2
    Views:
    298
    nutso fasst
    Oct 17, 2006
  5. Niall
    Replies:
    3
    Views:
    169
    Niall
    Dec 6, 2006
Loading...

Share This Page