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