4

從前端整合 WebForm 的各種方法

 2 years ago
source link: https://blog.darkthread.net/blog/reportviewer-with-js/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

從前端整合 WebForm 的各種方法

2021-07-16 05:06 PM 0 841

即便前端發展已十分成熟,幾乎無所不能,但仍有些必須依賴 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,這裡介紹四種不同做法:

  1. 透用 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;
    }
    
  2. 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 的無形表單如下:
     <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>   
    
    由於 st、ed、s 等 hidden 欄位已用 v-model 連動,故最上方的 <input type="date" type="date"> 及 <select> 輸入值將即時更新,而 form 設定 target="rptViewer",只需呼叫 .submit() 即可模擬 PostBack 並顯示在 IFrame 中:
    ShowP: function () {
         this.Loading = true;
         document.getElementById('rptForm').submit();
     }
    
    這麼做的好處是 DemoReport.aspx 可限定只接受 POST 請求,停用 GET 請求可避免惡意人士捏造 URL 發動 Cross-Site Scripting 攻擊。延伸閱讀:隱含殺機的 GET 式 AJAX 資料更新
  3. 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();
     }
    
  4. 最後一種做法稍稍複雜,但客製彈性最大,能支援加密或更複雜的安全管控,做法是前端透過 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,有興趣的同學可抓回去玩。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK