This example is an introduction to my thoughts on SPAs.
The web page uses the <asp:literal> as a place holder for where the variable content is to go.
Although this page was created to discuss SPAs it is also applicable to Razor/Blazor.
<html>
<head runat="server">
<link rel="stylesheet" type="text/css" title="CSS" media="(min-width: 800px)" href="/StyleSheets/Base_Max.css" />
</head>
<body>
<form id="form2" runat="server">
<div class="masthead_spa">
</div>
<div class="localmenu">
<uc:Menu runat="server" ID="Menu1" />
</div>
<asp:ScriptManager runat="server"></asp:ScriptManager>
<div class="Standard_Columns_20">
<h2 class="Standard_Columns_Sub_Header_Left">Because I Am A Luddite Here Is A SPA Using Just C#/.Net
</h2>
<div class="Standard_Columns_Details">
<div class="img_section">
<img src="/Images/Common/Me4.jpg" alt="me in the garden" style="width: 100%;" />
</div>
<br />
This part of the web site is a Single Page Application, apparently this is the Flavour Of The Month for web sites.<br />
<br />
Being fair, I too prefer this style a lot of the time, but building SPAs and MPAs (multi page) sites is done quite differently.
<br /><br />
</div>
</div>
<div class="Standard_Columns_80">
<asp:UpdatePanel ID="panelMessages" runat="server">
<ContentTemplate>
<div class="localmenu">
<uc:MenuSPA runat="server" ID="ucMenuSPA" />
</div>
<div class="container">
<asp:Literal ID="litAppArea" runat="server"></asp:Literal>
</div>
</ContentTemplate>
</asp:UpdatePanel>
</div>
<uc:Adverts runat="server" ID="Adverts1" CalledBY="-1" />
<uc:Footer runat="server" ID="Footer1" />
</form>
</body>
</html>
The menu sits in a user control (<uc:MenuSPA runat="server" ID="ucMenuSPA" />), basically a .Net include, the important point is the text box txtMode which is hidden and is used to control the view to be displayed.
Each of the menu links call a bit of javascript to set the new mode and then the asp.Net framework takes over and calls a page refresh server side.
In this case I have chosen to have the menu as a constant item, but it could be handled in just the same way as the rest of the changing page.
<ul class="localmenu2">
<li><asp:LinkButton OnClientClick="SetMode('Overview');" ID="lbOverview" runat="server">Welcome Page</asp:LinkButton></li>
.....
<li><asp:LinkButton OnClientClick="SetMode('Error');" ID="lbError" runat="server">Error Page</asp:LinkButton></li>
</ul>
<asp:TextBox ID="txtMode" runat="server" Style="display: none"></asp:TextBox>
<script>
function SetMode(NewMode)
{
strMode = document.getElementById("ucMenuSPA_txtMode");
strMode.value = NewMode;
return true;
}
</script>
Then the C# code behind provides the "routing" in the Page_Load event and passes a StringBuilder which will be populated with the necessary mark-up by whatever ends up handling the request.
It is assumed that any javascript required by this mark-up has already been uploaded to the web server and is included.
Each page or possibly one aspect of that page (display/update) is handled by a class dedicated to that task.
In this example there are two trivial classes, one that reads a database table and one that just displays static text included in the code behind for the page.
Normally I would expect each class, or possibly a group of classes to be build as a standalone dll.
In particular you can see that the class for the database access has this access built into it rather than calling a web service, this allows full access to the .Net framework as well as your company's own libraries along with no time being wasted on which library in javascript/Go do I need to call to query SQL Server along with Oh, that was unexpected! Why do I need to set ....!
using System;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
public partial class SPA_Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
StringBuilder objHTMLText = new StringBuilder(32000);
RouteRequest(ref objHTMLText);
this.litAppArea.Text = objHTMLText.ToString();
}
protected void RouteRequest(ref StringBuilder objHTMLText)
{
// First time through always go to the home page
if (IsPostBack == false)
{ // Build an overview page
OverviewPage objOverviewPage = new OverviewPage();
if (objOverviewPage.GetHTML(ref objHTMLText) == false) SetError(ref objHTMLText, "Page Not Built");
}
else
{
// Recover the mode, not that the variable is in the user control ucMenuSPA.ascx
String strMode = Request.Form["ucMenuSPA$labMode"].ToString();
switch (strMode)
{
case "Overview":
OverviewPage objOverviewPage = new OverviewPage();
if (objOverviewPage.GetHTML(ref objHTMLText) == false) SetError(ref objHTMLText, "Page Not Built correctly");
break;
case "SharesBigDrops":
StocksPage objStocksPage = new StocksPage();
if (objStocksPage.GetHTML(ref objHTMLText) == false) SetError(ref objHTMLText, "Page Not Built correctly");
break;
default:
SetError(ref objHTMLText, "There is no handler for the page " + strMode + " in the Default.aspx.cs.Page_Load()");
break;
}
}
}
protected void SetError(ref StringBuilder objHTMLText, String strErrorMsg)
{
objHTMLText.Clear();
objHTMLText.Append(@"<h1 class=""Standard_Columns_Sub_Header"">SPA ""Framework Error</h1>
<div class=""Standard_Columns_Details"">
<div class=""Standard_Columns_Details_Big"">
There was an error building the web page
</div>" + strErrorMsg);
}
}
/* In the real world each of these classes would be compiled into a dll*/
public class OverviewPage
{
public Boolean GetHTML(ref StringBuilder objHTMLText)
{
objHTMLText.Append(@"<h1 class=""Standard_Columns_Sub_Header"">An SPA Using Only C# and .Net
</h1>
<div class=""Standard_Columns_Details"">
<div class=""Standard_Columns_Details_Big"">
Normally SPAs are created using javascript libraries like React.
</div>
<div class=""Standard_Columns_Split2"">
A detailed explanation on how this page works is available < a href = ""Explained.aspx"" > here </ a >.< br >< br >
Basically the idea is to implement each page as a C# class possibly in a dll that provides the mark-up that is dropped into an <asp:literal>
<br /><br />This is explained on a separate page to keep the code clear for the explanation.
</div></div>");
return true;
}
}
public class StocksPage
{
public Boolean GetHTML(ref StringBuilder objHTMLText)
{
String strSQL;
SqlConnection connSQL;
SqlCommand cmdSQL;
SqlDataReader drSQL;
Int32 i32Lc = 0;
objHTMLText.Append(@"<h1 class=""Standard_Columns_Sub_Header"">Some Big Share Drop Prices</h1>
<div class=""Standard_Columns_Details"">
<div class=""Standard_Columns_Details_Big"">
This could be part of a my shares portfolio site.
</div>Shown below are some of the latest big share price drops.<br /><br />");
objHTMLText.Append(@"<div class=""DataForm"">
<table style=""width:100%"">
<thead>
<tr>
<th>Date</th>
<th>Ticker</th>
<th>Company</th>
<th>% Loss</th>
<th>Reason</th>
</tr>
</thead>
<tbody>");
strSQL = @"[spSelect tab_5_BigDrops2]";
using (connSQL = new SqlConnection(ConfigurationManager.ConnectionStrings["AConnectionstringName"].ToString()))
using (cmdSQL = new SqlCommand())
{
connSQL.Open();
cmdSQL.Connection = connSQL;
cmdSQL.CommandType = CommandType.StoredProcedure;
cmdSQL.CommandTimeout = 3600;
cmdSQL.CommandText = strSQL;
using (drSQL = cmdSQL.ExecuteReader())
{
while (drSQL.Read() == true)
{ // Add details to hml table
objHTMLText.Append("<tr>");
objHTMLText.Append("<td>"); objHTMLText.Append(drSQL.ReturnFieldAsDateTimeFormated("DateRecorded", "dd/MMM/yyyy"));
objHTMLText.Append("</td>");
objHTMLText.Append("<td title=\"" + drSQL.ReturnFieldAsStringOrSpace("Company") + "\">");
objHTMLText.Append(drSQL.ReturnFieldAsStringOrSpace("Ticker")); objHTMLText.Append("</td>");
objHTMLText.Append("<td>"); objHTMLText.Append(drSQL.ReturnFieldAsStringOrSpace("Company")); objHTMLText.Append("</td>");
objHTMLText.Append("<td>"); objHTMLText.Append(drSQL.ReturnFieldAsDecimalOrZero("PercentageLoss")); objHTMLText.Append("%</td>");
objHTMLText.Append("<td>"); objHTMLText.Append(drSQL.ReturnFieldAsStringOrSpace("Reason")); objHTMLText.Append("</td>");
objHTMLText.Append("</tr>");
i32Lc++;
}
drSQL.Close();
}
}
// Did we find any data
if (i32Lc == 0)
{ // No
objHTMLText.Clear();
objHTMLText.Append("No Latest Big Drops found!");
}
else
{ // Yes
// Close table
objHTMLText.Append(@"</tbody></table>");
}
objHTMLText.Append(@"</div></div>");
return true;
}
}
I am hoping that at this point you can see where I am going; for example you could have two methods for each field returned from a query that provides the mark-up for a read only view or an updatable view.
Where I do have concerns about this approach is with very high volume sites, where every wasted millisecond is critical.
Balancing the workload concern against the fact that this approach keeps the developer solidly in the mainstream of the Microsoft environment where he is the most familiar and there are a lot of candidates to choose from when they move on.
Remember the days when jQuery ruled the world?