Web App Builder - 适用于任何移动设备的单页应用程序(SPA)






4.92/5 (20投票s)
即时生成 Web 应用程序,直接从数据库 (SQL Server) 生成
引言
单页应用程序 (SPA) 被定义为一种 Web 应用程序,它位于一个 Web 页面上,目标是提供与桌面应用程序类似的更愉快的用户体验。
以下示例应用程序代码是使用 AngularJS、Knockout 等库的替代方案。除 jQuery 和 boostrap 等成熟库外,还结合使用了 JavaScript、HTML 和 CSS。
一种非常简单的方法是叠加 iframe 或对象以及 jQuery Post 例程 & JSON,来读取和更新数据库,而无需任何回发。
此应用程序中的 Grid 和 Detail 表单还包含简单的 CSS,使其能够自动调整大小以适应任何移动设备,直到 iPhone 等。通过水平和垂直滚动或滑动,用户可以快速读取 Grid 中的所有数据列和行。
包含的演示应用程序可以从 Visual Studio 2012/2013 和 Express 版本中打开。只需按照提供的readme.txt文件中的说明定义 SQL Server 数据库即可。
编辑/添加时的 Grid 记录持久性
此应用程序可以通过使更新的记录即时可用,来维护和冻结大型数据库中的已更新或添加的记录,即使已修改排序的列。记录保持当前可用,直到准备好进行下一次搜索,从而大大提高性能。
示例应用程序是用 ASP.NET C# Framework 4.0 编写的,具有 Razor 页面和 Entity Framework - Entity SQL 数据库操作。
使用代码,使其看起来像一个桌面应用程序
使其成为 SPA 应用程序的主要逻辑是展开DIV
并叠加 iframe/对象,然后通过删除叠加的 iframe 来返回。
iframe 通过包含 iframe 破解代码和web.config文件中的行条目来保护。
iframe 破解代码
function IframeCSSCheck() {
//Check if the top location is same as the current location
if (top.location.hostname != self.location.hostname) {
//If not then set the top to you current
top.location.href = self.location.href;
alert('iframe trying to be busted, please wait...');
}
}
web.config
add name="X-Frame-Options" value="SAMEORIGIN"
用于叠加 iframe 并再次隐藏的 JavaScript 代码
//Javascript Functions to expand DIV with iframe and then return back
function OpenDetails(PKID, row_ID, arrayname) {
var iframeid = "iframeNamesdetail"; //DIV in form
var iframdiv = document.getElementById(iframeid);
document.getElementById('Name_ID').value = PKID; //Primary Key
//Url.Action(Action, Contoller)
var pagesrc = '(Url.Action("NamesShow","Names"))'; //Run Controller to
//display record details and linked child grid
var frameHeight = document.getElementById('NamesForm').offsetHeight;
var frameWidth = document.getElementById('NamesForm').offsetWidth;
var ifrm = "iframeupdDetailNames" + row_ID;
var iframdivsrc = "<iframe id=" + ifrm + " style='overflow: no;
border: 0px' width=" + frameWidth + " height=" + frameHeight + " frameborder='0'" +
"scrolling='no' src=" + pagesrc + "></iframe>";
iframdiv.innerHTML = iframdivsrc;
var topset = document.getElementById('NamesForm').offsetTop;
var topsetchr = topset + "px";
var leftset = document.getElementById('NamesForm').offsetLeft;
var leftsetchr = leftset + "px";
document.getElementById(iframeid).style.display = "block";
document.getElementById(iframeid).style.position = "absolute";
document.getElementById(iframeid).style.top = topsetchr;
document.getElementById(iframeid).style.left = leftsetchr;
}
//Used to Erase the overlayed iframe
function MinimDetail() {
var realparentrowid = parent.document.getElementById('CurrentRowNumber').value;
var iframerowid = "iframeupdDetailNames" + realparentrowid;
if (parent.document.getElementById(iframerowid) != null) {
parent.document.getElementById(iframerowid).src = "about:blank"; //set
//iframefrom to blank src, from parent form.
parent.document.getElementById(iframerowid).style.display = "none";
}
}
遍历示例应用程序
主 Grid 到子逻辑和代码
如上图所示,初始起点来自NamesGrid.cshtml页面,该页面是从菜单布局页面和控制器NamesController.cs (DisplayNames
) 调用的。NamesGrid.cshtml然后渲染另外两个页面(即Names.cshtml & NamesResults.cshtml)。Names.cshtml通过 jQuery Post 调用自动从数据库读取Names
表,然后将结果 JSON 数据渲染到NamesResults.cshtml页面上。
NamesController
/// Display master Grid Page
/// Initial start point of displaying the Grid
public ActionResult DisplayNames()
{
Models.NamesViewModel ThisViewModel = new Models.NamesViewModel();
ThisViewModel.TotalPages = 0;
ThisViewModel.TotalRows = 0;
ThisViewModel.CurrentPageNumber = 0;
ThisViewModel.SortAscendingDescending = "DESC"; //default init setting
ViewData.Model = ThisViewModel;
return View("NamesGrid"); //must match View name
}
/// Get actual records from Database
public PartialViewResult NamesSearch()
{
long totalRows;
long totalPages;
bool returnStatus;
string returnErrorMessage;
NamesBLL ThisBLL = new NamesBLL();
Models.NamesViewModel ThisViewModel = new Models.NamesViewModel();
this.TryUpdateModel(ThisViewModel); //get search criteria form input
List<names> scripts = ThisBLL.NamesSearch(
ThisViewModel,
ThisViewModel.CurrentPageNumber,
ThisViewModel.PageSize,
ThisViewModel.SortBy,
ThisViewModel.SortAscendingDescending,
out totalRows,
out totalPages,
out returnStatus,
out returnErrorMessage);
ViewData["scripts"] = scripts; //give back list array to View(ie ...Results) for processing(reading)
ThisViewModel.TotalPages = totalPages;
ThisViewModel.TotalRows = totalRows;
ViewData.Model = ThisViewModel;
return PartialView("NamesResults"); //must match View name
}
为了显示链接到每个主记录的后续子 Grid,另一个页面叠加在主 Grid 页面之上,通过展开DIV
并使用 jQuery Post 调用显示NamesShowDetail.cshtml页面及其链接的子 Grid 记录,如上图所示。
Detail JavaScript
function OpenDetails(PKID, row_ID, arrayname) {
NamesShowProgressBar();
resetablebg();
HideChildALL();
rowhighlight(row_ID);
var iframeid = "iframeNamesdetail"; //div in Names.cshtml
var iframdiv = document.getElementById(iframeid);
document.getElementById('Name_ID').value = PKID; //hidden on Names.cshtml form, for later child processing
//Url.Action(Action, Contoller)
var pagesrc = '@(@Url.Action("NamesShow","Names"))'; //show record details
var frameHeight = document.getElementById('NamesForm').offsetHeight;
var frameWidth = document.getElementById('NamesForm').offsetWidth;
var ifrm = "iframeupdDetailNames" + row_ID;
var iframdivsrc = "<iframe id=" + ifrm + " style='overflow: no;
border: 0px' width=" + frameWidth + " height=" + frameHeight + " frameborder='0'" +
"scrolling='no' src=" + pagesrc + "></iframe>";
iframdiv.innerHTML = iframdivsrc;
var topset = document.getElementById('NamesForm').offsetTop;
var topsetchr = topset + "px";
var leftset = document.getElementById('NamesForm').offsetLeft;
var leftsetchr = leftset + "px";
document.getElementById(iframeid).style.display = "block";
document.getElementById(iframeid).style.position = "absolute";
document.getElementById(iframeid).style.top = topsetchr;
document.getElementById(iframeid).style.left = leftsetchr;
}
//Used to hide the iframeDetailNames[i] from the above OpenDetails form
function MinimDetail() {
var realparentrowid = parent.document.getElementById('CurrentRowNumber').value;
var iframerowid = "iframeupdDetailNames" + realparentrowid;
if (parent.document.getElementById(iframerowid) != null) {
parent.document.getElementById(iframerowid).src = "about:blank"; //set
// iframefrom to blank src, from parent form.
parent.document.getElementById(iframerowid).style.display = "none";
}
$("#iframeNamesdetail").html(""); //also erases everything inside DIV, which has iframe
//Next is so to hightlight row in grid that has been either updated or added
if (document.getElementById('Name_ID').value != 0) {
newrowhighlight(); //highlight row on page, by traversing
// grid page of records and finding updated PK_ID
}
return;
}
}
从 Names 主 Grid 中,通过 Grid 中每个记录上的链接,通过 jQuery 对话框调用来启动弹出式 Detail 页面。
Popup JavaScript
//-----------------------------------------------------------------------------------
//Used to show jquery Dialog Edit/Add popup (PopDialog(-1,0) is for Add
//----------------------------------------------------------------------------------
function PopDialog(PKID, row_ID) {
$("#NamesForm #Name_ID").val(PKID); //Primary Key
var putTitle = "";
if (PKID == -1) {
putTitle = "Add Names";
}
else {
putTitle = "Edit Names";
resetablebg();
rowhighlight(row_ID);
}
var strAction = "NamesDetail";
var strController = "Names";
//Url.Action(Action, Contoller)
var pagesrc = '@(@Url.Action("NamesDetail7","Names7"))'; //new replace code example
pagesrc = pagesrc.replace("NamesDetail7", strAction);
pagesrc = pagesrc.replace("Names7", strController);
var dialogWidth = 0;
var topform = top.document.forms[0].id; //use this to get TOP Master GRID form
dialogWidth = document.getElementById(topform).offsetWidth;
$("#modalIframeId").attr("src", pagesrc);
$("#divId").dialog({
autoOpen: true,
modal: true,
title: "",
width: dialogWidth,
height: 400,
position: "center",
resizable: false,
draggable: true
});
$("#divId").dialog("option", "title", putTitle);
}
//--------------------------------------------------------------------------------------
//END show jquery Dialog popup (PopDialog(-1,0) is for Add
//-------------------------------------------------------------------------------------
子到孙子逻辑和代码
如上图所示,主 Grid 和子 Grid 页面NamesShowDetail.cshtml现在叠加在主页面NamesGrid.cshtml之上。子 Grid(即Client_CareGrid.cshtml)位于Names
详细信息下方。
所有Child
和GrandChild Grid
程序代码都相同,只与主 Grid 不同,因为
主代码中未使用外键链接的数据库键。
应用程序的架构设置如此,您可以轻松添加更多的GreatGrandChilds
等。
Grid 自动调整大小以适应移动设备
HTML 和 CSS 代码
<div id="idDivNamesTBody" style="width: auto;
height: auto; overflow: scroll; -webkit-overflow-scrolling: touch;"
onscroll="myTimer();" >
<table id="NamesTable" style="width: 100%; min-width: 700px; height: auto;">
Grid rows & columns ..........................
</table>
</div>
一个 DIV 包围着一个 Table,该 Table 具有最小宽度,从而导致水平和垂直
出现滚动条。可以修改最小宽度以适应使用的 Grid 列。
编辑/添加时的 Grid 记录持久性
Entity SQL 代码
var customers = from c in customerQuery
select new { c.Name_ID, c.Firstname, c.Lastname, c.Date_of_Birth, c.Update_Date };
int numberOfRows = customers.Count();
var customersunion = from c in customerQuery
select new { c.Name_ID, c.Firstname, c.Lastname, c.Date_of_Birth, c.Update_Date };
if (SearchValues.Name_ID != 0) //Update or Add
{
customerQueryADD = customerQueryADD.Where(c => c.Name_ID == SearchValues.Name_ID);
customersunion = from c in customerQueryADD
select new { c.Name_ID, c.Firstname, c.Lastname, c.Date_of_Birth, c.Update_Date };
}
//if PageSize = -1, then creating PDF Report or Spreadsheet instead, so get all records.
var customerList = customers;
if (pageSize == -1)
{
customerList = customers;
}
else
{
if (SearchValues.Name_ID != 0) //doing Update or Add a record
{
if (numberOfRows == 0) //always set record count to at least 1
{
numberOfRows = 1;
}
customerList = customersunion.Union(customers.Skip((Convert.ToInt32
(currentPageNumber) - 1) * Convert.ToInt32(pageSize)).Take(Convert.ToInt32(pageSize)));
}
else
{
customerList = customers.Skip((Convert.ToInt32(currentPageNumber) - 1) *
Convert.ToInt32(pageSize)).Take(Convert.ToInt32(pageSize));
}
}
如果 Grid 记录正在被更新或添加,那么该记录将始终包含在最终选定的记录中
通过执行union
。这确保用户始终能在当前页面上看到该记录。
http://youtu.be/q2XpEvzcYqY - 编辑/添加时的 Grid 记录持久性
演示视频
下面是演示 YouTube 视频,展示了此应用程序的主要功能。
- http://youtu.be/QZifItvYleM - 导航
- http://youtu.be/xsQ25cwTNVE - 调整大小以适应任何设备
- http://youtu.be/q2XpEvzcYqY - 编辑/添加时的 Grid 记录持久性
- http://youtu.be/g8dI9a-Y_Zw - 登录安全
关注点
我在这方面创建 SPA 应用程序的方法上冒险了,我使用了<iframe>
标签,这有一些反对者。但它是 HTML5 规范的一部分,Google 和 YouTube 也在使用它们。HTML5 标签<object>
可以代替 iframe 使用,并且我为此选项提供了代码。但是,我确实是从内部 JavaScript 创建所有 iframe,并且我还使用 iframe 破解 JavaScript 以及web.config中的域控制代码,以防止任何 iframe XSS 外部攻击。我还使用了一种非常简单的方法,用于调整所有屏幕以适应所有设备,尤其是Grid
,这些 Grid 通过 CSS 显示水平和垂直滚动条。
历史
- 2014 年 10 月 10 日:初始版本