65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (20投票s)

2014年10月10日

CPOL

4分钟阅读

viewsIcon

42131

downloadIcon

1257

即时生成 Web 应用程序,直接从数据库 (SQL Server) 生成

Sample Image - maximum width is 600 pixels

Sample Image - maximum width is 600 pixels

引言

单页应用程序 (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 到子逻辑和代码

Sample Image - maximum width is 600 pixels

如上图所示,初始起点来自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;
    }        
}

Sample Image - maximum width is 600 pixels

从 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
    //-------------------------------------------------------------------------------------

子到孙子逻辑和代码

Sample Image - maximum width is 600 pixels

如上图所示,主 Grid 和子 Grid 页面NamesShowDetail.cshtml现在叠加在主页面NamesGrid.cshtml之上。子 Grid(即Client_CareGrid.cshtml)位于Names详细信息下方。

所有ChildGrandChild Grid程序代码都相同,只与主 Grid 不同,因为
主代码中未使用外键链接的数据库键。
应用程序的架构设置如此,您可以轻松添加更多的GreatGrandChilds等。

Grid 自动调整大小以适应移动设备

Sample Image - maximum width is 600 pixels

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 记录持久性

Sample Image - maximum width is 600 pixels

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 视频,展示了此应用程序的主要功能。

关注点

我在这方面创建 SPA 应用程序的方法上冒险了,我使用了<iframe>标签,这有一些反对者。但它是 HTML5 规范的一部分,Google 和 YouTube 也在使用它们。HTML5 标签<object>可以代替 iframe 使用,并且我为此选项提供了代码。但是,我确实是从内部 JavaScript 创建所有 iframe,并且我还使用 iframe 破解 JavaScript 以及web.config中的域控制代码,以防止任何 iframe XSS 外部攻击。我还使用了一种非常简单的方法,用于调整所有屏幕以适应所有设备,尤其是Grid,这些 Grid 通过 CSS 显示水平和垂直滚动条。

历史

  • 2014 年 10 月 10 日:初始版本
© . All rights reserved.