一、要實現一個功能,在不同的頁面放置一段如下的內容,用於采集用戶行為信息:
<input type='hidden' id='page_id' value='xxxx' /> <script type="text/javascript"> //balabala... </script>
[1] 需求中還藏着一點,有些頁面加,有些頁面不加。
二、方案
方案一:當然可以這樣做:找到需要采集的頁面,一個個打開將采集代碼拷貝進去,然后把xxxxx修改為分配給各頁面的值。但如此顯然違背DRY原則。
方案二:不希望采集代碼處處被粘貼的話,那么從View的角度,備選的機制有partial view和layout 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/ViewData。layout 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