2018/04/27

SampleCMS 網頁共用元件說明:簡介

在製作 ASP.NET Web Forms 網頁程式時,如果遇到多個網頁程式都會寫到的重覆功能,常用的做法就是將這項目功能從這些網頁程式中抽出並且放在一個共用的父類別 BasePage 之中,讓所有網頁程式都繼承自 BasePage,共用 BasePage 裡面的功能。

以我的例子來說,我喜歡把四散在網站中所有網頁程式裡的 Request.QueryString["Id"], Request.QueryString["name"] 包裝成屬性 int qsId, string qsName。
(另外,Session["Param"] 和 ViewState["Param"] 也是)

會這麼做的起因是為了弱點掃描,當初為了要針對 QueryString 參數值撰寫白名單機制,在統計 Request.QueryString["xxx"] 全部有多少參數名稱、各是什麼樣的類型的時候,覺得四散在程式裡的相同參數名稱太多,而且 QueryString[] 很自由的讓參數名稱不用區分大小寫導致同名參數有不同的大小寫在不同的地方,覺得維護時不好讀。

另外若參數值目的是用在非字串類型的時候,通常就需要有轉型的程式碼。
例如 Request.QueryString["Id"] 參數值要以數值類型來用,10 個使用 Request.QueryString["Id"] 的地方,大概就會有 8 個地方要寫 Convert.ToInt32() ,甚至還會有 null 判斷或最大最小值判斷,這樣子重覆的程式碼我覺得透過屬性包裝會比較好維護一點。
(而且在 Visual Studio 還有 IntelliSense 這樣的好處,


因此像屬性 int qsId, string qsName 這樣的共用功能,我就放到 BasePage 裡面,
而 UserControl 也可以透過 ((BasePage)this.Page) 來使用共用的功能。

不過後來遇到一個問題,我希望 .ashx 的程式也可以使用上述的共用功能,但是 .ashx 是 Handler 不是 Page,不能使用 BasePage。因此我更改了網頁程式繼承 BasePage 的方式,改用網頁程式去使用 BasePage 元件的方式(後來改名 PageCommon)。這樣子的話,不管是網頁程式 (Page.aspx)、UserControl (.ascx)、Handler (.ashx),都是以相同的方式使用共用元件的共用功能。

為了在 PageCommon 新增程式的時候,也能夠有像是在 BasePage 新增程式的體驗,主要就是取網址參數時還是寫 Request.QueryString["Param"]、用 Session 參數還是寫 Session["Param"],因此 PageCommon 需要生出 Request, Response, Session, ViewState 這些常用的屬性。以下是實作方式。
using System.Web;
using System.Web.SessionState;
using System.Web.UI;

    public class PageCommon
    {
        protected HttpContext context;
        protected StateBag viewState;

        public PageCommon(HttpContext context, StateBag viewState)
        {
            this.context = context;
            this.viewState = viewState;
        }

        #region 工具屬性

        protected HttpServerUtility Server
        {
            get { return context.Server; }
        }

        protected HttpRequest Request
        {
            get { return context.Request; }
        }

        protected HttpResponse Response
        {
            get { return context.Response; }
        }

        protected HttpSessionState Session
        {
            get { return context.Session; }
        }

        protected System.Security.Principal.IPrincipal User
        {
            get { return context.User; }
        }

        protected StateBag ViewState
        {
            get { return viewState; }
        }

        protected System.Web.Caching.Cache Cache
        {
            get { return context.Cache; }
        }

        #endregion
    }

上述程式碼主要的重點在 Line 10,只要 Page, UserControl, Handler 將它們的 this.Context 傳進來,PageCommon 就有 Server, Request, Response, Session, User, Cache 可用。ViewState 因為不在 Context 裡面,所以需要 Page 和 UserControl 將它們的 this.ViewState 從第二個參數傳進來。

以上就是網頁共用元件 PageCommon 的由來。

在 SampleCMS 之中,由於前後台網頁有不同需求的共用功能,所以以 PageCommon 做為網頁共用元件的基底類別,只要是前後台網頁都用得到的功能就放 PageCommon。
SampleCMS 的 PageCommon
後台網頁共用元件 BackendPageCommon 主要共用的功能就是「取得權限判斷用資料、存取登入資訊、控制後台 UI 元件... 等」,這些只有後台網頁常用的功能。

前台網頁共用元件 FrontendPageCommon主要共用的功能是「取得直接開啟或是預覽模式的網頁代碼、取得指定的網頁資料、客製化網頁的相關功能...等」,這些前台專用的功能。

BackendPageCommon & FrontendPageCommon

最後,由於後台的權限規則設計每一種資料管理功能都需要一個專屬的管理頁共用元件,因此從 BackendPageCommon 再衍生出各個管理頁共用元件,例如「帳號管理」的共用元件為 AccountCommonOfBackend。

而前台因為有客製化網頁程式要處理,因此從 FrontendPageCommon 再衍生出 OtherArticlePageCommon 來製作客製化網頁程式。

SampleCMS 整個網頁共用元件關聯圖如下,

One more thing ...
當初從 BasePage 改用 PageCommon 的時候,為了減少轉換架構造成的不便感,每個網頁程式裡的第一個網頁共用元件,不管其類型名稱是什麼,變數名稱皆以 c 命名, c 取自 common 的頭文字 c。
這樣子原本的程式碼例如 GetData(qsId); 只要加個 c. 改成 GetData(c.qsId); 即可。

而當初這樣子的命名規則就被我一直延用下去,直到現在的 SampleCMS 也是。
下列以 Account-List.aspx.cs 為例,列出有使用網頁共用元件 AccountCommonOfBackend 的地方。
public partial class Account_List : BasePage
{
    protected AccountCommonOfBackend c;
    // ...略...
    protected void Page_PreInit(object sender, EventArgs e)
    {
        c = new AccountCommonOfBackend(this.Context, this.ViewState);
        c.InitialLoggerOfUI(this.GetType());
        c.SelectMenuItemToThisPage();

        empAuth = new EmployeeAuthorityLogic(c);
        // ...略...
    }
    // ...略...
    protected void Page_Load(object sender, EventArgs e)
    {
        hud.RebuildBreadcrumbAndUpdateHead(c.GetOpIdOfPage());
        // ...略...

        if (!IsPostBack)
        {
            if (!empAuth.CanOpenThisPage())
            {
                Response.Redirect(c.BACK_END_HOME);
            }

            // ...略...
        }
        // ...略...
    }

    private void LoadUIData()
    {
        // ...略...

        //condition vlaues
        ddlEmpRange.SelectedValue = c.qsEmpRange.ToString();
        ddlDept.SelectedValue = c.qsDeptId.ToString();
        txtKw.Text = c.qsKw;

        // ...略...

        c.DisplySortableCols(new string[] { 
            "DeptName", "RoleSortNo", "EmpName", 
            "EmpAccount", "StartDate", "OwnerName"
        });
    }
    // ...略...
    private void DisplayAccounts()
    {
        AccountListQueryParams param = new AccountListQueryParams()
        {
            ListMode = c.qsEmpRange,
            DeptId = c.qsDeptId,
            Kw = c.qsKw
        };

        param.PagedParams = new PagedListQueryParams()
        {
            BeginNum = 0,
            EndNum = 0,
            SortField = c.qsSortField,
            IsSortDesc = c.qsIsSortDesc
        };

        param.AuthParams = new AuthenticationQueryParams()
        {
            CanReadSubItemOfOthers = empAuth.CanReadSubItemOfOthers(),
            CanReadSubItemOfCrew = empAuth.CanReadSubItemOfCrew(),
            CanReadSubItemOfSelf = empAuth.CanReadSubItemOfSelf(),
            MyAccount = c.GetEmpAccount(),
            MyDeptId = c.GetDeptId()
        };

        // get total of items
        empAuth.GetAccountList(param);

        // update pager and get begin end of item numbers
        int itemTotalCount = param.PagedParams.RowCount;
        ucDataPager.Initialize(itemTotalCount, c.qsPageCode);
        if (IsPostBack)
            ucDataPager.RefreshPagerAfterPostBack();

        param.PagedParams = new PagedListQueryParams()
        {
            BeginNum = ucDataPager.BeginItemNumberOfPage,
            EndNum = ucDataPager.EndItemNumberOfPage,
            SortField = c.qsSortField,
            IsSortDesc = c.qsIsSortDesc
        };

        DataSet dsAccounts = empAuth.GetAccountList(param);

        if (dsAccounts != null)
        {
            rptAccounts.DataSource = dsAccounts.Tables[0];
            rptAccounts.DataBind();
        }

        if (c.qsPageCode > 1 || c.qsSortField != "")
        {
            ClientScript.RegisterStartupScript(this.GetType(), "isSearchPanelCollapsingAtBeginning", "isSearchPanelCollapsingAtBeginning = true;", true);
        }
    }
    // ...略...
}




沒有留言:

張貼留言