Shortly after writing my last post, I started thinking of better ways to create a .NET MVC Single Page application. So here it is.
First, I tried translating this to .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# .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 (controller == null) {
return false;
}
var url = "/" + controller + "/";
if (action != null) {
url += action + "/";
}
if (id != null && action != null) {
url += id + "/";
}
return url;
}
function updateNavBar(action) {
$(".nav").find(".active").removeClass("active");
if (action != null) {
$(".nav").find("#" + action).addClass("active");
} else {
$(".nav").find("#home").addClass("active");
}
}
function getPage(controller, action, id) {
var url = buildMVCURL(controller, action, id);
var data = { Controller: controller, Action: action, ID: id };
$("#spaBody").load(url);
window.history.pushState(data, null, url);
$(".navbar-collapse").collapse('hide');
updateNavBar(action);
}
window.addEventListener('popstate', function(e) {
var url = buildMVCURL(e.state.Controller, e.state.Action, e.state.ID);
$("#spaBody").load(url);
$(".nav").find(".active").removeClass("active");
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, it's 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 ids 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 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:
Live Example!
See a live example on Microsoft Azure here:
Original SPA Test Post
CodeProject
The post DotNet MVC Single Page Application Take Two! appeared first on Blank's Tech Blog.