Shortly after writing my last post I started thinking of better ways to create a DotNet MVC Single Page application. So here it is.
First I tried translating this to Dot Net Core but unfortunately it doesn’t have a method for Request.IsAjaxRequest() and I’m not ninja enough to write my own yet (although I did try) so this example remains in C# Dot Net MVC 4.5.2.
Step 1 – Layout Magic
The magic of this version starts with the _ViewStart.cshtml page (/Views/_ViewStart.cshtml) where we add a little line to detect if the incoming request is from Ajax or a normal url request from a browser. This is important because even though we can load pages via Ajax if a URL to that page is bookmarked and used later we will need to send the page back with the layout. However the page requested via ajax doesn’t need one so we don’t send it. The line looks like this:
@{ Layout = Request.IsAjaxRequest() ? null : "~/Views/Shared/_Layout.cshtml"; }
Credit is due here: http://stackoverflow.com/questions/5318385/mvc-3-how-to-render-a-view-without-its-layout-page-when-loaded-via-ajax
Step 2 – Javascript MVC Voodoo
Now that we have the Layout loading dynamically it’s time to change how we request the pages once the layout is loaded. That’s where this little bit of Javascript comes in:
<script> function buildMVCURL(controller, action, id) { //If there is no controller return false if (controller == null) { return false; } //Start URL string with /controller/ var url = "/" + controller + "/"; //If action is available add to URL /controller/action/ if (action != null) { url += action + "/"; } //Finally is id is available add it to URL string /controller/action/id/ if (id != null && action != null) { url += id + "/"; } //Return url string return url; } function updateNavBar(action) { //Remove active class from Nav Bar $(".nav").find(".active").removeClass("active"); //Add active class to Nav Bar link by ID if (action != null) { $(".nav").find("#" + action).addClass("active"); } else { $(".nav").find("#home").addClass("active"); } } function getPage(controller, action, id) { //Build URL var url = buildMVCURL(controller, action, id); //Write parameters to data object for navigation history var data = { Controller: controller, Action: action, ID: id }; //Load url into the DOM at ID #spaBody $("#spaBody").load(url); //Push History to address bar with data for history navigation window.history.pushState(data, null, url); //Collapse navbar in mobile view $(".navbar-collapse").collapse('hide'); //Update Bootstrap Nav Bar state updateNavBar(action); } window.addEventListener('popstate', function(e) { //build URL from back button history state var url = buildMVCURL(e.state.Controller, e.state.Action, e.state.ID); //Load url into the DOM at ID #spaBody $("#spaBody").load(url); //Remove active class from Nav Bar $(".nav").find(".active").removeClass("active"); //Update Bootstrap Nav Bar state updateNavBar(e.state.Action); }); </script>
** Updated script 10/5/2016: Updated script to actually navigate on back/forward buttons and when in mobile view close the navbar on link click. Also added updating active state on Bootstrap NavBar. **
I placed the above script in _Layout.cshtml (Views/Shared/_Layout.cshtml) for now but it should probably live in a file somewhere else that gets bundled and minified in the initial page request. #ToDo for Part 3 if I ever make one#
What this script does is accepts the parameters of a normal MVC page request and creates a url from it. Then it uses that url to load the page via Ajax (which gets returned without a layout because of the previous step) and loads it into the DOM. Then it pushes that URL to the browser history and URL in the browser bar so the user can use the forward and back browser navigation and bookmark the page if desired.
Step 3 – Linking Pages
To use the script in the page links its quite simple and looks as follows
<div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li id="home"><a href="javascript:getPage('home')">Home</a></li> <li id="SpaTest"><a href="javascript:getPage('home','SpaTest')">SpaTest</a></li> <li id="About"><a href="javascript:getPage('home','About')">About</a></li> <li id="Contact"><a href="javascript:getPage('home','Contact')">Contact</a></li> </ul> </div>
**Update 10/5/2016: added id’s to List Items so the active state can be updated**
Step 4 – Giving your script a target
In the layout page around the “@RenderBody tag I’ve added a div with an id to load the response html into like this:
<div class="container body-content" > <div id="spaBody"> @RenderBody() </div> <hr/> <footer> <p>© @DateTime.Now.Year - My ASP.NET Application</p> </footer> </div>
Step 5 – Testing where we came from
The above steps are really all that’s needed to get this working but how can we know it’s truly working as designed? Test it!
In the controller action for Spa Test I added some test logic.
public ActionResult SpaTest() { ViewBag.AjaxRequest = false; if (Request.IsAjaxRequest()) { ViewBag.AjaxRequest = true; } return View(); }
Then I added the output to the view:
<div> <div class="jumbotron"> <h1>Spa Test!</h1> <p class="lead"> I am content that was loaded via @if (ViewBag.AjaxRequest) {<Text>Ajax</Text>} else {<text>full get request to server</text>} ! </p> </div> </div>
*Note above is the entire view code. I am no longer forcing a null layout as I did in the original SPA Test post because of the changes in step 1.
Now that we have that code added when we view the Spa Test page from the nav bar link (via ajax) we get this:
However if we click on the URL in the address bar and hit enter or hit F5 (forcing the browser to request the full page) we get this:
Another good way to check is by using the network tab on your web browser’s developer tools (F12 in Chrome). When loading via the URL you will see a higher load time and more files come down with the response like so:
Yet when loaded via Ajax it’s a much smaller response and response time:
Thank you & Please share!
Again thanks for reading, especially if you made it this far, and feel free to comment (and tell me what I’m doing right or wrong) and share so more people can join the fun!
Full Source Code:
Code is updated here for your enjoyment so go ahead and #ForkMe:
https://github.com/andyrblank/MVCSpa
Live Example!
See a Live example on Microsoft Azure here:
http://mvcspatest.azurewebsites.net/
Original SPA Test Post:
http://blankstechblog.com/load-html-dom-ajax-net-mvc/
Leave a Reply