Good afternoon, dear Khabarovsk. The site has
repeatedly raised the issue of creating single-page ajax applications. I faced this problem some time ago. However, I wondered why, having the capabilities of
html5 and the power of MVC, I have to prescribe so much by
hand, and even with the help of js .
Perhaps it was
[holywar = on] hostility to the js language
[holywar = off] that prompted me to create a simple solution based on ASP.NET MVC features. Next, I will describe in detail the problems that arise when trying to create a one-page ajax application, and will gradually consider creating a complete solution.
If it became interesting - welcome under the cat (code and pictures are attached).
So, we will experiment with the standard MVC 3 template application. We will remove all unnecessary from it to get the project configuration, approximately as in the figure below and we will start implementing the loading step by step.

')
Stage 1 - Problem Analysis
When I first met
Ajax.ActionLink I thought - Aha! What could be easier, to implement the entire application in this way! The main block in the page template (_Layout.cshtml) is identified as main and we load all the other pages into it. However, problems immediately arise:
- The user moved to a new page and expects that the "Back" button will return it to the previous one, but this does not happen - the user returns not to the previous one, but to the first fully loaded page. The "Forward" button also does not work.
- The user moved to a new page and wanted to refresh it (for example, using F5), but instead of updating the current page, the first fully loaded page is loaded again.
- User opened link to new page in new tab / window. However, instead of opening the normal page, PartialView loaded without the main template, styles and scripts.
- The user walked through the pages of the site, then became distracted and wanted by the name of the tab (window) to find the page with which he worked before. But the name of the page is in _Layout.cshtml and therefore also has not changed in the process of navigation.
- Distract from navigation. The user went to the data entry page he needed, but ... client data validation on the page that ajax returned does not work!
- And now the user followed the link, which pulls out 100,500 records from the database and performs operations on them. And then once again I followed the same link, as I decided that “something wasn’t working for me again”. And then another. And after that, it would be better if the user did not wait for the end of all these requests, this will start happening on the page ... =)
There were not enough problems. And not all of them are solved by simple js code. Here we need an integrated approach to architecture. And if so, then immediately add a couple more requirements:
- Suppose that when loading a new page, the current one is simply blocked and a message is displayed to the user about what is happening in the system right now.
- Let the navigation through the history be simple and intuitive, as if the application works in the usual multi-page mode.
- As I said, I do not like js. Let the maximum amount of code be written in c #. And on js we will write only about 10 lines.
Finally adopted technical limitations:
- We will use html5 chips.
- We will not engage in layout and design. Just make our portal function properly.
- Our solution should be as easy to use as the portal becomes more complex. Immediately we will put in it support of areas, the main part of the functionality will be rendered in Ajax / Html helpers.
- To simplify the decision, let's take a few magical naming conventions. These agreements will be discussed below.
Problems are described, goals set, go!
Stage 2 - Implementing the Controller
Let's start with the controller. Obviously, in order to load both parts of pages using ajax, as well as pages completely (in case the user follows the link or refreshes the page), actions that would return both PartialViewResult and ViewResult are required.
Therefore, in the controller for each logical action we place two physical ones - one of the
SomeAction: ViewResult , the second of
AjaxSomeAction: PartialViewResult . The
Ajax prefix in this case is the first magical naming convention. Let us continue to use this fact.
Total we get:
public class HomeController : Controller { private Object GenerateIndexPage() { Object model = null; for (int i = 0; i < 1000000000; i++) { } return model; } [HttpGet] public ViewResult Index() { return View("Index", GenerateIndexPage()); } [HttpGet] public PartialViewResult AjaxIndex() { return PartialView("Index", GenerateIndexPage()); } [HttpGet] public ViewResult About() { return View("About"); } [HttpGet] public PartialViewResult AjaxAbout() { return PartialView("About"); } }
The GenerateIndexPage () method in this simple way emulates some long operations, such as accessing a database.
Stage 3 - _Layout.cshtml
Now let's do a general template. I quote his code right away:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> AjaxNavigation</title> <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript")></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript")></script> <script src="@Url.Content("~/Scripts/ajaxnavigation.js")" type="text/javascript")></script> </head> <body> <div id="loadLayout" style="display: none; position: fixed; z-index: 20; top: 0px; left: 0px; width: 100% !important; height: 100% !important;"> <div style="margin-left: -24px; margin-top: -24px; position: relative; top: 50%; left: 50%; z-index: 20;"> <div id="loadMessage"> </div> <div> <img src="@Url.Content("~/Content/progress.gif")" alt="..." /> </div> </div> </div> <div class="page"> <header> <nav> <ul id="menu"> <li>@Ajax.ActionLinkTo(" ", "Index", "Home", "", " ...")</li> <li>@Ajax.ActionLinkTo(" ", "About", "Home", "", " ...")</li> </ul> </nav> </header> <div id="main"> @RenderBody() </div> </div> </body> </html>
So, first of all we connect the jquery libraries. We load them all at once, so as not to create unnecessary problems for ourselves with manual reloading during ajax navigation. We will write the
ajaxnavigation.js script to write further and this file will be very small ~ 10 lines of code, as I mentioned earlier.
The title of the page has quite specific content - the name of our application. In principle, you can leave it empty - we will assign it with js each time the page is loaded manually.
The
loadLayout and
loadMessage elements are defined to show the loading process. The elements themselves are designed in such a way as to block the user from clicking anywhere without hitting while the new page is loading.
The block named
main contains the body of the page itself. We will update its contents using ajax. Together with the two previous blocks (loadLayout and loadMessage), the main block also has a magic name, we will use it later.
Ajax.ActionLinkTo (...) creates a link with all associated parameters. Consider it in detail below.
Stage 4 - Ajax.ActionLinkTo (...)
This helper is a small extension over the standard
Ajax.ActionLink (...) :
public static class AjaxHelpers {
Helper creates the
necessary route taking into account the area (after
all, many very real projects have areas), and it turned out to be extremely convenient to set them together with the action and the controller. We will need this route in order to insert it into the address bar of the browser during ajax navigation through pages, as well as for keeping navigation history.
The
AjaxOptions object will replace the body of the main block with the received content and execute two js functions (more about them later):
- changeLoadMesage - changes the text that will be displayed to the user during the process of loading a new page.
- onPageLoaded - called after a successful page load and performs some actions related to the address bar and history.
It should also be noted that in fact, even when we called the helper, we specified the name of the action, as
Index / About , the helper will substitute the above-mentioned
Ajax prefix and will cause exactly the action intended for the Ajax request, i.e.
AjaxIndex / AjaxAbout .
Etam 5 - magic script ajaxnavigation.js
It is time for a script that contains only three functions. First, the elementary function of changing the label that appears when loading:
Secondly, the function called after loading part of the page:
First of all, we need to get the url of the loaded page to substitute this url in the browser line, and also save it in the history of visits. To do this, each page is placed hidden block pageUrl. After taking the link to the page, we add a new state to the stack of window states, marking it with the code word "
ajax ". This is done in order to navigate through the history (when the user pokes forward / back) to force the browser to reload the page.
We also need to change the page title and update the embedded client validation.
The third function is called when the page loads:
It also changes the title of the document, and also signs for loading any state from the history. If the state is marked as "
ajax ", then reload the page, if not, then simply mark it in such a way for subsequent reloading.
Stage 6 - Presentation and Html.PageInfo
A typical representation in our case looks like this:
@Html.PageInfo(" ","/Home/Index") <h2>Home</h2> <p> Home page </p>
The
Html.PageInfo (...) helper adds hidden blocks with the title of the document and a link to the current page. As we saw earlier, these hidden blocks are used in scripts. The helper is very simple:
public static class HtmlHelpers {
Stage 7 - Intermediate Summary
Now you can run our application, not forgetting to connect the helpers created by html / ajax in the
web.config of the Views folder:
<system.web.webPages.razor> ... <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> <add namespace="AjaxNavigation.Core"/> </namespaces> </pages> </system.web.webPages.razor>
We can quickly test the application with such a sequence, I illustrated it with screenshots:
- Download page / Home / Index;
- Make sure that the About link leads to / Home / AjaxAbout;
- Follow the link and make sure that it is loaded using ajax and the link / Home / About appears in the address bar;
- Follow the Index link, which also indicates the action / Home / AjaxIndex;
- Make sure that during a long-term operation a message is displayed and it is impossible to follow any other link;
- After loading the Index page, you will make sure that the history has been preserved and navigation is available (“Back” and “Forward”);
- Make sure that after switching to any page, updating the page works correctly.



There is one harmful problem. The opening of our link in a new tab / window still does not work:

Stage 8 and the last - Open in a new tab ...
Let's try to understand what the problem is. Obviously, when the browser tries to open the
/ Home / AjaxIndex link in a new tab, no ajax will be executed. In this case, we need to return the full view of the page, and not just a part of it. But how to determine that the browser has opened a new window?
For this we need some kind of diagnostic tool, I use
Fiddler , a very simple and fast traffic analyzer. Run it and compare two requests that the browser sends to the server:
- with the usual link following
- when trying to open a link in a new window


Now it is clear that in the case of a real ajax request, the browser appends the
X-Requested-With header. It remains to write a filter of incoming requests, which would redirect us from a partial page to the full.
No sooner said than done:
public abstract class ControllerBase : Controller { protected override void Execute(RequestContext requestContext) {
Well, do not forget, to inherit
all of our controller from
ControllerBase .
Source code and useful links
It is possible with
ifolder.ru or with
zalil.ru .
Thanks for attention! Successful codding.