ASP.NET MVC - PageData的應用

yicone發表於2013-07-02

一、要實現一個功能,在不同的頁面放置一段如下的內容,用於採集使用者行為資訊:

<input type='hidden' id='page_id' value='xxxx' />
<script type="text/javascript">
    //balabala...
</script>

[1] 需求中還藏著一點,有些頁面加,有些頁面不加。

二、方案

方案一:當然可以這樣做:找到需要採集的頁面,一個個開啟將採集程式碼拷貝進去,然後把xxxxx修改為分配給各頁面的值。但如此顯然違背DRY原則。

方案二:不希望採集程式碼處處被貼上的話,那麼從View的角度,備選的機制有partial viewlayout pages。如果使用前者,仍然需要被採集頁面,重複的引用包含採集程式碼的partial view。如果使用後者,假使網站已經使用了多個layout pages(這很常見),依然會需要重複的粘帖採集程式碼。

1. 那麼我們將兩者結合起來,並且做一點折衷,用paritial view包含採集程式碼,然後在各layout pages中引用partial view:

@Html.Partial("_TraceByPageIdScript")

讓採集程式碼就可以安然存在於_TraceByPageIdScript這個partial view中。

2. 作為page_id值的xxxx怎麼辦?

開始的想法是,在被採集的頁面中,對ViewBag.PageId賦值(set),保證在該變數被使用前有值(不為null)。然後以為在_TraceByPageIdScript中就可以使用ViewBag.Page了(get)。

實驗可恥的失敗鳥。

發現pages的ViewBag和layout pages的ViewBag是不同的物件,從屬於不同的WebViewPage例項。
類似的,包括MVC3之前的ViewData,都不是用在"pages需要和layout pages共享資料"這種場景的。
[it] Gets or sets a dictionary that contains data to pass between the controller and the view.
http://msdn.microsoft.com/en-us/library/system.web.mvc.viewpage.viewdata.aspx

google到了PageData
[it] Provides array-like access to page data that is shared between pages, layout pages, and partial pages.
http://msdn.microsoft.com/en-us/library/system.web.webpages.webpagebase.pagedata(v=VS.99).aspx

所以採集程式碼中的xxxx,可以替換為PageData["xxxx"]了

3. 這個時候發現犯了一個錯誤,斷點看了下,PageData["xxxx"]在partial pages中的值是null。(這個和俺對MSDN註釋的理解不大一致啊)
換個方式吧:

@Html.Partial("_TraceByPageIdScript", (int?)PageData["PageId"], new ViewDataDictionary())

注意:如果這裡省掉了第三個引數,那麼當null被傳遞到partial view時,會導致實際傳入的物件型別是@model指明的型別,進而導致異常。這是ASP.NET MVC 3的一個古怪的地方,尚不清楚後續版本是否有調整。
如下兩篇資料給自給了一種解法:

  a. http://stackoverflow.com/questions/650393/renderpartial-with-null-model-gets-passed-the-wrong-type
    @Html.Partial("_TraceByPageIdScript", new ViewDataDictionary(PageData["PageId"]))

  b. http://stackoverflow.com/questions/9292852/how-do-i-invoke-a-partial-view-with-null-for-its-model
    @Html.Partial("_TraceByPageIdScript", (int?)PageData["PageId"], new ViewDataDictionary())

4. 還有前面的需求[1]被丟下了,在_TraceByPageIdScript.cshtml中,對採集程式碼包一下:

@model int?
@if(Model.HasValue)
{
    <input type='hidden' id='page_id' value='@Model.Value' />
    <script type="text/javascript">
        //balabala...
    </script>
}

三、結語

小經周折,至此完整的實現了該需求。

除了ASP.NET MVC相關的知識外,“值即開關”模式的使用,實現了隨需載入採集程式碼的效果,這樣今後如果其它頁面需要加入採集,只需要在相應頁面,通過賦值即可;不再需要採集的頁面,賦值為null或者注視掉賦值語句即可。

更理想的方案,其實是被採集的頁面,完全不用為了被採集而進行任何修改。

做法之一,以配置的形式維護一個字典,value是page_id,key則要求可以唯一標識某個特定頁面。如果有了這個字典,程式可以在執行時根據請求,來找到key,進而就能找到page_id。以此作為基礎,就可以利用一個能共享給layout pages的變數,來告訴layout pages是否開啟"輸出採集程式碼"的開關。

這裡有兩個比較複雜的點:

  1. 以何載體來作為這裡說的共享變數?這也牽扯到根據請求找到page_id的時機。時機方面可以考慮Action Filter機制,這樣自然就能利用上Controller和Pages間的橋樑:ViewBag/ViewDatalayout pages拿到它,心裡一定很樂呵。
http://www.manasinc.com/setting-a-viewbag-property-in-the-onresultexecuting-action-filter-in-asp-net-mvc/

  2. 關鍵的難點在於,字典的key的選取。如果使用url,則當route發生變化時,相應頁面的採集就會失效。如果使用route,還要考慮引數取值的變化。

考慮到這個實現方案的複雜性,開發和維護都需要付出更多的精力,就沒有進一步的探索下去了。


P.S. PageData["XXX"] 等效於 Page.XXX

相關文章