28

Im working on a project in MVC and have enjoyed learning about it. There are a few growing pains but once you figure them out it's not bad. One thing that is really simple in the WebForms world is maintaining the scroll position on a page. All you do is set the MaintainScrollPositionOnPostback property to true. However, in MVC, Im not using postbacks so this will not work for me. What is the standard way of handling this?

Edit: Ajax is acceptable, but I was also wondering how you would do it without AJAX.

Papa Burgundy
  • 6,397
  • 6
  • 42
  • 48
  • If you are using AJAX and jQuery then select a visible element that is topmost on your page. Say it is an anchor with no text Then in your $(document).ready write $('#pageTop').focus(); – Vishnoo Rath Oct 24 '13 at 11:52

12 Answers12

56

I've resolved this in JS :

$(document).scroll(function () {
  localStorage['page'] = document.URL;
  localStorage['scrollTop'] = $(document).scrollTop();
});

Then in document ready :

$(document).ready(function () {
  if (localStorage['page'] == document.URL) {
    $(document).scrollTop(localStorage['scrollTop']);
  }
});
Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
Madagaga
  • 869
  • 9
  • 11
  • 3
    Very Good. When using a routing engine like asp.net mvc, the page in local storage may not match, but it really isn't needed if you use a unique name for the local storage scroll value... $(document).scroll(function () { localStorage['myuniquename'] = $(document).scrollTop(); }); $(document).ready(function () { $(document).scrollTop(localStorage['myuniquename']); }); – tintyethan Oct 23 '13 at 17:08
  • I used to use this in my asp.net applications, but now that I use MVC4 it just doesn't want to work anymore. The momand and amount it scrolls down seems so random. I have no idea whats going on. I put breakpoints and see that its doing "scrollTop(166)", but the browser is just ignoring it, other times it works fine, and sometimes it scrolls only a little bit of what its supposed to. – Jon Koeter Mar 30 '15 at 08:20
  • I've just tried with a Internet Application MVC4 project. It works. Maybe there is a script conflict or depending on your browser. I added the snippet after `@Scripts.Render("~/bundles/jquery")` – Madagaga Apr 02 '15 at 10:52
  • Great Solution, but the page is flickering on a page refresh in asp.net app. Is there any solution to that? – Suhani Apr 11 '19 at 16:49
  • This definitely works, but I'm struggling with the user who choses to go back into that same form within their session, and the form auto scrolls them to whatever position they were on when they either canceled or saved. Is there any way to clear the localStorage to avoid this? I tried localStorage.clear() in several locations on my scripts section but I'm still unable to get rid of this behavior. – David Arias Feb 02 '21 at 16:59
  • You have to detect when user goes back or add a expiration date – Madagaga Feb 03 '21 at 17:20
  • Update : You can use SessionStorage instead of LocalStorage. – Madagaga Mar 15 '23 at 19:22
8

The way MaintainScrollPositionOnPostback works is that it has a pair of hidden fields: __SCROLLPOSITIONX and __SCROLLPOSITIONY

On a postback, it sets these,

function WebForm_GetScrollY() {
    if (__nonMSDOMBrowser) {
        return window.pageYOffset;
    }
    else {
        if (document.documentElement && document.documentElement.scrollTop) {
            return document.documentElement.scrollTop;
        }
        else if (document.body) {
            return document.body.scrollTop;
        }
    }
    return 0;
}

function WebForm_SaveScrollPositionSubmit() {
    if (__nonMSDOMBrowser) {
        theForm.elements['__SCROLLPOSITIONY'].value = window.pageYOffset;
        theForm.elements['__SCROLLPOSITIONX'].value = window.pageXOffset;
    }
    else {
        theForm.__SCROLLPOSITIONX.value = WebForm_GetScrollX();
        theForm.__SCROLLPOSITIONY.value = WebForm_GetScrollY();
    }
    if ((typeof(this.oldSubmit) != "undefined") && (this.oldSubmit != null)) {
        return this.oldSubmit();
    }
    return true;
}

and then it calls RestoreScrollPosition:

function WebForm_RestoreScrollPosition() {
    if (__nonMSDOMBrowser) {
        window.scrollTo(theForm.elements['__SCROLLPOSITIONX'].value, theForm.elements['__SCROLLPOSITIONY'].value);
    }
    else {
        window.scrollTo(theForm.__SCROLLPOSITIONX.value, theForm.__SCROLLPOSITIONY.value);
    }
    if ((typeof(theForm.oldOnLoad) != "undefined") && (theForm.oldOnLoad != null)) {
        return theForm.oldOnLoad();
    }
    return true;
}

But as most people said, MVC should be avoiding postbacks anyway.

Lukas
  • 1,699
  • 1
  • 16
  • 49
Richard Gadsden
  • 839
  • 1
  • 6
  • 22
6

Actually there is no standard way of handling this, this was a Microsoft hack to support their post back model. They needed this because every control did a post back and the user would constantly be pushed back to the top of the page.

The recommendation for use with MVC is to do most of your post back to servers using AJAX. So that the page doesn't have to rerender the the focus is not moved. jQuery makes AJAX really easy, and there is even default forms like

<% Ajax.BeginForm(...) %>

Which will take care of the AJAX side of things for you.

Nick Berardi
  • 54,393
  • 15
  • 113
  • 135
4

Taking inspiration from WebForms and the answer provided by Richard Gadsden, another approach using javascript and the form collection could look something like this:

@{
    var scrollPositionX = string.Empty;        
    if(IsPost) {
        scrollPositionX = Request.Form["ScrollPositionX"];
    }
}

<form action="" method="post">
    <input type="hidden" id="ScrollPositionX" name="ScrollPositionX" value="@scrollPositionX" />
    <input type="submit" id="Submit" name="Submit" value="Go" />
</form>

$("#Submit").click(function () {
    $("#ScrollPositionX").val($(document).scrollTop());
});

$("#ScrollPositionX").each(function () {
    var val = parseInt($(this).val(), 10);
    if (!isNaN(val))
        $(document).scrollTop(val);
});

The code provided is for inspiration and is in no way prettified. It could probably be done in a few different ways, I guess it all comes down to how you decide to persist the scrollTop value of your document across the POST. It is fully working and should be cross browser safe since we are using jQuery to do the scrolling. I believe the code provided is self-explanatory, but I will be happy to provide a more detailed description on whats going on, just let me know.

Sniffdk
  • 316
  • 3
  • 5
  • An extra thank you from me. I put the last 3 lines into my $(document).ready() section and it all worked great. – Rocklan Nov 19 '12 at 23:01
3

I used name attributes in tags. No javascript used.

The page that I wanted to return to had <a> tags with name attribute, e.g. <a name="testname">.

The page (view) I returned from used tag <a href="<%: Request.UrlReferrer %>#testname">Back</a>". Request.UrlReferrer is used to go to previous page. #testname scrolls the page position to tag with name "testname".

the1johan
  • 31
  • 1
3

My own workaround is using some info in the ViewData to know what area must be shown in the backnavigation, and a little javascript to position the page's cursor:

In the View, an element like this:

<h3 id="tasks">
    Contained tasks
</h3>

And the javascript to repositionate the page:

<script type="text/javascript">
    addOnLoad(goAnchor);

    function goAnchor() {
        var paging = <%= //Here you determine (from the ViewData or whatever) if you have to position the element %>;
        if (paging == "True") {
            window.location.hash = "tasks";
        }
</script>

You could use a switch to determine what element from the view page you must relocate.

Hope it helps.

mapache
  • 1,361
  • 9
  • 11
2
<%
   if(!ViewData.ModelState.IsValid)
   {
%>
   window.location.hash = 'Error';
<%
   }
%>

 <a name="Error"></a>
Mike Flynn
  • 22,342
  • 54
  • 182
  • 341
1

Here's a simple, pure Javascript solution which I've tested in FF4 and IE9 only.

The idea is that this solution should degrade gracefully by falling back to the standard #anchor tags on a page. What I'm doing is replacing those #anchor tags on the fly with the X and Y coordinates, then on load, I simply read those values from the querystring and scroll there. If this fails for some reason, the browser should still navigate to the #anchor position...

Markup:

<a href="/somecontroller/someaction/#someanchor">My Link</a>

jQuery:

$(function() {

// RESTORE SCROLL POSITION
RestoreScrollPosition();

// SAVE SCROLL POSITION
$('a:not(a[href^="http"])').filter('[href$="#someanchor"]').each(function() {
    $(this).click(function() {
        var href = $(this).attr('href').replace("#someanchor","");
        if (href.indexOf('?') == -1) {
            href = href + '?x='
        } else {
            href = href + '&x='
        }
        href = href + window.pageXOffset;
        href = href + '&y=' + window.pageYOffset;
        $(this).attr('href', href);
    });
});
}

A couple of helper methods:

function RestoreScrollPosition() {

    var scrollX = gup('x');
    var scrollY = gup('y');

    if (scrollX != null && scrollY != null) {
        window.scrollTo(scrollX, scrollY);
        return true;
    }
    return false;
}

function gup(name) {
    name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
    var regexS = "[\\?&]" + name + "=([^&#]*)";
    var regex = new RegExp(regexS);
    var results = regex.exec(window.location.href);
    if (results == null)
        return "";
    else
        return results[1];
}

This fits my needs, but could be more generic/reusable - I'd be happy for someone to improve on this... :-)

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
user744322
  • 11
  • 1
0

I use .scrollTop like shown below, very easy, it even works with multiple forms in the view (I have a very long view, broken down into multiple forms):

First put this property inside the model:

public string scrollTop { get; set; }

And in the view, inside Form #1:

@Html.HiddenFor(m => m.scrollTop, new {@id="ScrollForm1"})

inside Form #2:

@Html.HiddenFor(m => m.scrollTop, new {@id="ScrollForm2"})

inside Form #2:

@Html.HiddenFor(m => m.scrollTop, new {@id="ScrollForm3"})

and then at the bottom of the view:

$(document).ready(function () {
    $(document).scrollTop(@Model.scrollTop);
        $(document).scroll(function () {
            $("#ScrollForm1").val($(document).scrollTop());
            $("#ScrollForm2").val($(document).scrollTop());
            $("#ScrollForm3").val($(document).scrollTop());
        });
});

Your scroll position is always preserved upon postback because the @Html.HiddenFor fields store your current scroll and pass it to the model on post. And then, when the page comes up it gets the scrollTop value from the model. At the end your page would behave like webform, everything stays intact.

Lukas
  • 1,699
  • 1
  • 16
  • 49
Yogi
  • 410
  • 4
  • 16
0

a very not-nice way to do this is using cookies.

If you use ONE page in your MVC which handles the other pages you could a code-snippet to it that loads every page which creates a cookie (if non-existent) called "scrolltop". There are ways to have javascript automatically update this cookie when the user scrolls up or down by catching these events or watching the scrollTop value.

On a new page you just have to load the saved position and make the view scroll there in 0 milliseconds (with Mootools or any Ajax script this should be possible) and the user will be exactly where they were.

I don't know much about asp so I don't know if a method exists to anchor to a current y-position. Javascript is a fast and easy way. Anchors in HTMl could be an option if you had every element anchored and posted the anchor to other pages.

xaddict
  • 1,302
  • 3
  • 19
  • 38
0

10 years on and a different JS solution. The other JS solution waits for the page to scroll and when the page loads scrolls to whatever position was saved. That's fine for probably most folks (although it doesn't remove the value so the page would always scroll to that position when you reviewed that page...). My solution is to wait for the form to submit:

(Yes, it is using jQuery, but the site has a lot of it...)

// OWNER'S FORM POSITION
if ($("[js-owner-request]").length) {
    $("[js-owner-request]").on("submit", function() {
        localStorage['owner-request__scrollTop'] = $(this).offset().top;
    });

    if (localStorage['owner-request__scrollTop'] !== "null") {
        $(document).scrollTop(localStorage['owner-request__scrollTop']);
        localStorage['owner-request__scrollTop'] = "null"; // set to null so we don't always scroll...
    }
}

This scrolls back to the top of the form, that way any error messages would be visible as you might have scrolled beyond the validation summary.

Lukas
  • 1,699
  • 1
  • 16
  • 49
Colin Wiseman
  • 848
  • 6
  • 10
-1

@{

}

<html>

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

window.onload = function () {
    var div = document.getElementById("dvScroll");
   var div_position = document.getElementById("div_position");
    var position = parseInt(@Request.Form("div_position"));
    if (isNaN(position)) {
        position = 0;
    }

    div.scrollTop = position;
    div.onscroll = function () {
        div_position.value = div.scrollTop;
    };
};

</script>
</head>

<body>

<div id="dvScroll" style="overflow-y: scroll; height: 260px; width: 300px">

    1. This is a sample text

    <br />

    2. This is a sample text

    <br />

    3. This is a sample text

    <br />

    4. This is a sample text

    <br />

    5. This is a sample text

    <br />

    6. This is a sample text

    <br />

    7. This is a sample text

    <br />

    8. This is a sample text

    <br />

    9. This is a sample text

    <br />

    10. This is a sample text

    <br />

    11. This is a sample text

    <br />

    12. This is a sample text

    <br />

    13. This is a sample text

    <br />

    14. This is a sample text

    <br />

    15. This is a sample text

    <br />

    16. This is a sample text

    <br />

    17. This is a sample text

    <br />

    18. This is a sample text

    <br />

    19. This is a sample text

    <br />

    20. This is a sample text

    <br />

    21. This is a sample text

    <br />

    22. This is a sample text

    <br />

    23. This is a sample text

    <br />

    24. This is a sample text

    <br />

    25. This is a sample text

    <br />

</div>

<hr />
<form method="post">
<input type="hidden" id="div_position" name="div_position" />
<input type="submit" value="Cool" />
    </form> 
</body>
</html>

You can use this to maintain scroll Position after postback.

Source: http://www.aspsnippets.com/Articles/Maintain-Scroll-Position-of-DIV-on-PostBack-in-ASPNet.aspx

John007
  • 7
  • 2