Pjax: pushState and Ajax Using ASP.NET Webforms






4.88/5 (4 votes)
Pjax ASP.NET webform is simple code that uses Ajax and pushState to deliver a fast browsing experience with real permalinks, page titles, and a working back button.
Introduction
Pjax ASP.NET webforms is working by loading *.aspx pages from your server via Ajax and replacing the content of a page working area <div class="working-area">
<!-- *.aspx page`s content comes here using ajax--></div>
on your page with the Ajax result html.
And also updates the browser's current URL using pushState without reloading your page's layout or any resources (JS, CSS), giving the appearance of a fast, full page load. But really it's just Ajax and pushState.
You can easily reload your page (refresh it) any time and save website history to apply forward and back form your browser with target url page content.
Pjax ASP.NET webforms demo can be found here.
Background
You can see the main project of Pjax https://github.com/defunkt/jquery-pjax and its demo: http://pjax.herokuapp.com/aliens.html that may apply on *.html page and ASP.NET MVC itself not working with ASP.NET webforms.
Using the Code
In our code, we will use:
- Two masterpages (one of them for normal loading and the other for partial (Ajax) load).
- JavaScript code file (to handle website links navigation and partial loading with updating website url).
- C# base class for every inherited page (help us to differentiate between normal and clicked loading every *.aspx page)
We will keep all scripts and styles in the default masterpage Site.master
contains all elements for normal loading as the following code:
<%@ Master Language="C#" AutoEventWireup="true"
CodeFile="Site.master.cs" Inherits="SiteMaster" %>
<meta charset="utf-8" /><meta name="viewport"
content="width=device-width, initial-scale=1.0" />
<title></title>
<asp:placeholder runat="server">
<%: Scripts.Render("~/bundles/modernizr") %> </asp:placeholder>
<p><webopt:bundlereference path="~/Content/css" runat="server">
<link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" /> <%--
<base href="/" id="appUrl" />--%> </webopt:bundlereference></p>
<form runat="server"><asp:scriptmanager runat="server"> <Scripts>
<%--To learn more about bundling scripts in ScriptManager
see http://go.microsoft.com/fwlink/?LinkID=301884 --%>
<%--Framework Scripts--%>
<asp:ScriptReference Name="MsAjaxBundle" />
<asp:ScriptReference Name="jquery" />
<asp:ScriptReference Name="bootstrap" />
<asp:ScriptReference Name="respond" />
<asp:ScriptReference Name="WebForms.js"
Assembly="System.Web" Path="~/Scripts/WebForms/WebForms.js" />
<asp:ScriptReference Name="WebUIValidation.js"
Assembly="System.Web" Path="~/Scripts/WebForms/WebUIValidation.js" />
<asp:ScriptReference Name="MenuStandards.js"
Assembly="System.Web" Path="~/Scripts/WebForms/MenuStandards.js" />
<asp:ScriptReference Name="GridView.js"
Assembly="System.Web" Path="~/Scripts/WebForms/GridView.js" />
<asp:ScriptReference Name="DetailsView.js"
Assembly="System.Web" Path="~/Scripts/WebForms/DetailsView.js" />
<asp:ScriptReference Name="TreeView.js"
Assembly="System.Web" Path="~/Scripts/WebForms/TreeView.js" />
<asp:ScriptReference Name="WebParts.js"
Assembly="System.Web" Path="~/Scripts/WebForms/WebParts.js" />
<asp:ScriptReference Name="Focus.js"
Assembly="System.Web" Path="~/Scripts/WebForms/Focus.js" />
<asp:ScriptReference Name="WebFormsBundle" />
<%--Site Scripts--%>
</Scripts>
</asp:ScriptManager>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" runat="server" href="~/">Application name</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a runat="server" href="/">Home</a></li>
<li><a runat="server" href="/About">About</a></li>
<li><a runat="server" href="/Contact">Contact</a></li>
</ul>
<asp:LoginView runat="server" ViewStateMode="Disabled">
<AnonymousTemplate>
<ul class="nav navbar-nav navbar-right">
<li><a class="normal" runat="server"
href="/Account/Register">Register</a></li>
<li><a class="normal" runat="server"
href="/Account/Login">Log in</a></li>
</ul>
</AnonymousTemplate>
<LoggedInTemplate>
<ul class="nav navbar-nav navbar-right">
<li><a runat="server" class="normal"
href="/Account/Manage" title="Manage your account">Hello,
<%: Context.User.Identity.GetUserName() %>!</a></li>
<li>
<asp:LoginStatus runat="server" class="normal"
LogoutAction="Redirect" LogoutText="Log off"
LogoutPageUrl="~/" OnLoggingOut="Unnamed_LoggingOut" />
</li>
</ul>
</LoggedInTemplate>
</asp:LoginView>
</div>
</div>
</div>
<div class="container body-content" id="body-content">
<asp:ContentPlaceHolder ID="MainContent" runat="server">
</asp:ContentPlaceHolder>
</div>
<div class="container body-content">
<hr />
<footer>
<p>© <%: DateTime.Now.Year %> - My ASP.NET Application,
<span class="text-danger" title="Time did not change"><strong>Time:
</strong> <%: DateTime.Now.TimeOfDay %></span></p>
</footer>
</div>
</form>
<script src="/Scripts/handlePages.min.js"></script></form>
Then we add a new empty master page called Site2.master
as the following code:
<%@ Master Language="C#" AutoEventWireup="true"
CodeFile="Site2.master.cs" Inherits="Site2" %>
<head runat="server"> </head>
<asp:contentplaceholder id="MainContent"
runat="server"></asp:contentplaceholder>
The next step: we will create FeachMasterPage.cs C# base class inside App_Code
for website pages to sense type of page's loading by user click or normal loading, it describes as the following:
using System;
public abstract class FeachMasterPage : System.Web.UI.Page
{
protected void Page_PreInit(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(Request.QueryString["click"]))
this.MasterPageFile = "/Site2.master";
}
}
we use a Page_PreInit event that can fire before site master page and helps us to determine the masterpage based on way of page load (normally or by user click) see asp.net page lifecycle.
The next: Inside every page, we can inherit from our base class: FeachMasterPage
as the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
public partial class About : FeachMasterPage //Page
{
protected void Page_Load(object sender, EventArgs e)
{
// page code goes here..
}
}
The last step: creating Js file inside Scripts folder called handlePages.js to handle user click as the following:
//#region pages handler
(function () {
"use strict";
// featch page contents (html, js, css,...)
function loadPage(widgetUri) {
var containerObjLocal = $('#body-content'), $progress = $('#loading');
$progress.css('display', 'block');
containerObjLocal.html('<span class="center">
<i class="icon-spinner center icon-spin orange bigger-125"></i></span>');
$.get(widgetUri, function (response) {
console.log(response)
containerObjLocal.html(response).fadeIn('fast');
}).done(function () {
// init page plugins
}).fail(function () {
//error alert
alert('Please reload the page by pressing f5');
//window.location.href = path;
}).always(function () {
$('html,body').animate({ scrollTop: 0 }, 'slow');
// hide progress
$progress.css('display', 'none');
});
}
// change url without reloading page
function ChangeUrl(page, url) {
if (history.pushState) {
var oj = { Page: page, Url: url };
history.pushState(oj, oj.Page, oj.Url);
} else {
console.log("Browser does not support HTML5.");
}
}
// open page content and change url
function openPage(title, path) {
try {
ChangeUrl(title, path); // assign page url
var separator = path.indexOf('?') > -1 ?
'&' : '?', url = path + separator + 'click=yes';
// start load page contents
loadPage(url);
}
catch (ex) {
alert(ex);
}
}
// pages anchor click event
$(document).delegate('a[href]:not([href=#])', 'click', function (e) {
var path = $(this).attr('href');
// add content only flag
// filter link before call page contents
var isClick = true;
$.map(['www.', 'http', 'javascript',
'@', 'mailto', 'default.aspx', '#'], function (v, i) {
if (path.indexOf(v) > -1) {
isClick = false;
return;
}
});
// prevent partial loading in these cases:
// the link has class="normal" or has target attributes.
if (isClick && !$(this).attr('target')
&& !$(this).hasClass('normal')) {
e.preventDefault();
openPage($(this).text(), path);
}
});
// sense page history back and forward
window.onpopstate = function (e) {
// open this page
var obj = e.state;
if (obj && obj.Url) {
openPage(obj.Page, obj.Url);
}
}
})();
//#endregion
As shown in the latest JS snippet code, we find openPage
function that calls another changeUrl
function (for changing current url without reloading our webpage) and loadPage
function that helps in gathering page contents like html, Js, CSS,... and shows it inside <div id="body-content"></div>
in main master page Site.master
using ajax $.get() method.
Points of Interest
- Normal load or at page refresh (F5) or opening my page in a new tab, in this case our page will load normally throw default Site.master masterpage.
- When user clicks on any navigation link (as website menu), Js code will sense this event and then change current page url and render new target page contents inside
<div id="body-content"></div>
without page reload.
Pjax ASP.NET webforms demo: http://pjax.share-web-design.com/