從前端整合 WebForm 的各種方法
0 |
即便前端發展已十分成熟,幾乎無所不能,但仍有些必須依賴 ASP.NET WebForm 的場合 - 例如,在前端專案中整合既有系統的 WebForm 網頁。如果該系統成熟且穩定,年資還比你多三倍,嚷著把它換掉改用前端重寫,通常老闆想換掉的會是你。另外,有些技術目前只有 WebForm 解決方案,ReportViewer 便是一例,此時直接在前端網頁整合 WebForm 是省時省力的做法。
這篇就來談談從前端整合 WebForm 網頁的幾種做法。
我做了張指定日期區間及排序方式的 RDLC 查詢報表為範例,藉此示範如何用純前端製作查詢介面,整合 WebForm ReportViewer 顯示報表,整合時需傳遞參數控制報表查詢結果也較符合實務應用情境。
查詢前端我採用在 HTML 引用 vue.js 的輕前端寫法,做了一個簡單的條件輸入查詢介面 - report.html,包含兩個 <input type="date" > 輸入查詢區間起迄日期,一個 <select> 選取排序欄位,<div class="loading" v-show="Loading"> 則是前幾天介紹的手作版載入中動畫。
<div class="content" id="app">
<div id="op">
日期區間:
<input type="date" v-model="StDate" />
~
<input type="date" v-model="EdDate" />
排序:
<select v-model="SortBy">
<option v-for="opt in SortOptions" v-bind:value="opt">{{opt}}</option>
</select>
<br />
<button v-on:click="ShowQ()">
查詢 (QueryString)
</button>
<button v-on:click="ShowX()">
查詢 (XSS)
</button>
<button v-on:click="ShowP()">
查詢 (POST Form)
</button>
<button v-on:click="ShowR()">
查詢 (Param Trans)
</button>
</div>
<iframe id="rptViewer" name="rptViewer" v-bind:src="IFrameSrc"></iframe>
<div class="loading" v-show="Loading">
<div class="mask"></div>
<div class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>
</div>
報表部分為求簡單,就不連資料庫了,在記憶體用色彩名稱模擬一份玩家清單,其中以亂數產生註冊日期,當成查詢日期區間標的:
public class SimulateData
{
public static DataTable DataTable = null;
static SimulateData()
{
var t = new DataTable();
t.Columns.Add(new DataColumn("PlayerId", typeof(string)));
t.Columns.Add(new DataColumn("Name", typeof(string)));
t.Columns.Add(new DataColumn("RegDate", typeof(DateTime)));
t.Columns.Add(new DataColumn("Score", typeof(int)));
var rnd = new Random(9527);
int i = 1;
typeof(Color).GetProperties(BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public)
.Select(c => (Color)c.GetValue(null, null))
.ToList()
.ForEach(c =>
{
t.Rows.Add(
$"P{i++:000}",
c,
new DateTime(1990, 1, 1).AddDays(rnd.Next(10000)),
rnd.Next(32767));
});
DataTable = t;
}
}
RDLC 報表設計及呈現效果如下,最上方傳入三個 ReportParameter:@StDate、@EdData、@SortBy 以顯示起迄日期及排序依據:
顯示 ReportViewer 的 WebForm DemoReport.aspx,所有邏輯在 Page_Load() 寫完,它同時接受 POST Form 或 QueryString 傳入的 st (註冊區間起日)、ed (註冊區間迄日) 及 s (排序欄位) 參數作為查詢條件及排序依據。未輸入有效參數時則不顯示 ReportViewer。另外,程式支援經由 MemoryCache 傳遞查詢參數的做法,運作原理後面會介紹。
public partial class DemoReport : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//https://blog.darkthread.net/blog/report-viewer-infinite-loop/
//防止無窮迴圈
if (ScriptManager1.IsInAsyncPostBack) return;
var st = Request.Form["st"] ?? Request.QueryString["st"] ?? string.Empty;
var ed = Request.Form["ed"] ?? Request.QueryString["ed"] ?? string.Empty;
var s = Request.Form["s"] ?? Request.QueryString["s"] ?? string.Empty;
var p = Request["p"] ?? "NA";
var paramDict = (MemoryCache.Default.Get(p)) as Dictionary<string, string>;
if (paramDict != null)
{
st = paramDict["st"];
ed = paramDict["ed"];
s = paramDict["s"];
}
if (string.IsNullOrEmpty(st) || string.IsNullOrEmpty(ed) || string.IsNullOrEmpty(s))
{
//未傳參數時顯示空白
ReportViewer1.Visible = false;
}
else
{
ReportViewer1.Visible = true;
ReportViewer1.ProcessingMode = ProcessingMode.Local;
ReportViewer1.LocalReport.ReportPath = Server.MapPath("~/Reports/PlayerReport.rdlc");
//此處以 DataView 模擬查詢資料庫
DataView view = SimulateData.DataTable.DefaultView;
//指定排序依據
view.Sort = s;
//指定查詢區間
view.RowFilter =
$"RegDate >= '{DateTime.Parse(st):yyyy-MM-dd}' AND RegDate <= '{DateTime.Parse(ed):yyyy-MM-dd}'";
ReportDataSource ds = new ReportDataSource("DataSet1", view.ToTable());
ReportViewer1.LocalReport.DataSources.Clear();
ReportViewer1.LocalReport.DataSources.Add(ds);
ReportViewer1.LocalReport.SetParameters(new ReportParameter("StDate", st));
ReportViewer1.LocalReport.SetParameters(new ReportParameter("EdDate", ed));
ReportViewer1.LocalReport.SetParameters(new ReportParameter("SortBy", s));
}
}
}
從前端整合 WebForm 方法很多,核心概念是用 IFrame 嵌入 WebForm 頁面,至於觸發查詢及參數溝通有多種做法,可以走純 HTML 協定,也可利用 JavaScript XHR,這裡介紹四種不同做法:
- 透用 QueryString
最簡單直覺的做法,修改 IFrame src="DemoReport.aspx?st=...&ed=...&s=..." 代入參數顯示結果
這裡用 Vue 3 示範,將 <input> <select> 等輸入值繫結到 View Model 變數,按鈕時,將其組成 URL 指定給 IFrame 即完成所有動作。var vm = Vue.createApp({ data: function () { return { StDate: '2000-01-01', EdDate: new Date().toJSON().substr(0, 10), SortBy: 'PlayerId', SortOptions: [ 'PlayerId', 'Name', 'RegDate', 'Score' ], IFrameSrc: '../Reports/DemoReport.aspx', Loading: false }; }, methods: { ShowQ: function () { this.Loading = true; this.IFrameSrc = '../Reports/DemoReport.aspx?st=' + this.StDate + '&ed=' + this.EdDate + '&s=' + this.SortBy + '&_=' + new Date().getTime(); } } }).mount('#app'); function hideLoadingAnimation() { vm.Loading = false; }
- QueryString GET 方法有參數外露及易被 XSS 攻擊的缺點,可改從前端 POST Form 到 DemoReport.aspx
賦與 IFrame name="rptViewer",在前端宣告一個 <form target="rptViewer" action="DemoReport.aspx">,其中加入 <input name="st">、<input name="ed">、<input name="s">,送出表單時 DemoReport.aspx 進入 IsPostBack == true 流程,可由 Request.Form["st"]... 等取得參數,並由於指定了 target="rptViewer",DemoReport.aspx 會顯示在 IFrame。report.html 的無形表單如下:
由於 st、ed、s 等 hidden 欄位已用 v-model 連動,故最上方的 <input type="date" type="date"> 及 <select> 輸入值將即時更新,而 form 設定 target="rptViewer",只需呼叫 .submit() 即可模擬 PostBack 並顯示在 IFrame 中:<form action="../Reports/DemoReport.aspx" method="post" target="rptViewer" enctype="application/x-www-form-urlencoded" id="rptForm"> <input type="hidden" name="st" v-model="StDate" /> <input type="hidden" name="ed" v-model="EdDate" /> <input type="hidden" name="s" v-model="SortBy" /> </form>
這麼做的好處是 DemoReport.aspx 可限定只接受 POST 請求,停用 GET 請求可避免惡意人士捏造 URL 發動 Cross-Site Scripting 攻擊。延伸閱讀:隱含殺機的 GET 式 AJAX 資料更新ShowP: function () { this.Loading = true; document.getElementById('rptForm').submit(); }
- Cross-Frame Scripting,若前端網頁與 WebForm 同屬一個站台,基於同源原則,前端的 JavaScript 可伸手進 IFrame 操作網頁元素
在 DemoReport.aspx HTML 中加入隱藏的參數 input,由外部操作填入參數按鈕送出可模擬自身的 PostBack 行為。這種設計方式的好處是可在 DemoReport.aspx 埋入防止跨站請求偽造(Cross-Site Request Forgery,CSRF)的欄位、Cookie,或是使用 ASP.NET 內建的 ViewStateUserKey 防止請求偽造攻擊,提高網站安全性。
實際做法要在 DemoReport.aspx HTML 加入參數欄位,為了與方法 2 共用參數接收邏輯,我是用純 HTML input 欄位,若無此考量可改用 WebForm 控制項 <asp:TextBox> 等標準 WebForm 控制項,口味更道地。最下方的 Script 則於網頁載入後隱藏父層網頁的載入中動畫。
前端寫法如下:<form id="form1" runat="server"> <div style="display: none"> <input type="text" id="st" name="st" readonly /> <input type="text" id="ed" name="ed" readonly /> <input type="text" id="s" name="s" readonly /> </div> <asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager> <div id="divReport"> <rsweb:ReportViewer ID="ReportViewer1" runat="server" Height="100%" Width="100%"> </rsweb:ReportViewer> </div> <script> if (parent && parent.hideLoadingAnimation) parent.hideLoadingAnimation(); </script> </form>
ShowX: function () { var doc = document.getElementById('rptViewer').contentWindow.document; doc.getElementById('st').value = this.StDate; doc.getElementById('ed').value = this.EdDate; doc.getElementById('s').value = this.SortBy; this.Loading = true; doc.getElementById('form1').submit(); }
- 最後一種做法稍稍複雜,但客製彈性最大,能支援加密或更複雜的安全管控,做法是前端透過 WebAPI 將查詢參數存入伺服器端(例如:MemoryCache、DB)後取得 Token,將 Token 當作參數傳給 DemoReport.aspx,DemoReport.aspx 再以 Token 提取真正參數內容當查詢條件。
為此我寫了一簡單的 WebAPI 示範:
而前端則寫成:public class MvcApiController : Controller { public ActionResult SaveParam(string st, string ed, string s) { var key = Guid.NewGuid().ToString(); MemoryCache.Default.Add(key, new Dictionary<string, string> { ["st"] = st, ["ed"] = ed, ["s"] = s }, DateTime.Now.AddSeconds(30)); return Content(key); } }
ShowR: function () { var self = this; this.Loading = true; $.post("../MvcApi/SaveParam", { st: this.StDate, ed: this.EdDate, s: this.SortBy }).done(function (paramKey) { self.IFrameSrc = '../Reports/DemoReport.aspx?p=' + paramKey + '&_=' + new Date().getTime(); }); }
完整操作展示如下:
範例專案我已放上 Github,有興趣的同學可抓回去玩。
Example of embedding and communicating with WebForm from frond-end.
Comments
Be the first to post a comment