最近發現目前的 ASP.NET MVC 5 最新版 (v5.2.3) 在搭配 Visual Studio 2015 進行開發時,在 View 頁面中使用 @Html.IdFor() 或 @Html.NameFor() 在搭配使用特定 Lambda 語法時會輸出奇怪的字元,由於所有強型別的 HtmlHeper 表單欄位輸出的內部都會用到 IdFor() 與 Namefor() 這兩個 API,所以這個問題將會導致這些表單欄位 HTML 輸出的時候產生錯誤的 id 與 name 屬性,當表單 POST 回 Controller 時將無法正確執行模型繫結 (Model Binding),所以會有接不到資料的情況,本篇文章將詳加說明發生的原因與暫時的解決方案。
重現問題
我先來重現這個問題,執行環境的基本要求如下:
- 使用 Visual Studio 2015 開發工具
- 必須從 Visual Studio 2015 建立全新的 ASP.NET MVC 5 專案
我特別撰寫了一個重新此問題的專案,有興趣的人可以到以下網址下載我的測試專案:
這個專案內容很簡單,基本上我只寫了三個檔案:
1. 定義一個模型類別叫做 Person
2. 定義一個 HomeController 也很簡單,只有一個 Index 動作方法, 這裡準備了一個 List<Person> 的清單資料傳給 View 使用
而我的 View 則是模擬為了要做多筆資料的模型繫結寫法,先把傳入的模型轉成陣列,再透過一個 foreach 輸出每個欄位:
執行的時候你會看到以下畫面:
我們從 HTML 原始碼來看,你可以發現欄位的 id 與 name 屬性都怪怪的,這樣的名稱會導致當表單資料 POST 回 Controller 的時候無法正常執行模型繫結 (Model binding)!
這個專案有趣的地方就在於,完全相同的程式碼,如果從 Visual Studio 2013 建立一個全新的 ASP.NET MVC 5 專案開始,我們所得到的 HTML 輸出則是正常的,以下是正常的欄位名稱輸出,這才是我們原本預期的輸出結果,而這樣的欄位名稱才能讓我們正確執行模型繫結:
怪了,到底是什麼東西造成了這樣的差異?為什麼一定要用 Visual Studio 2015 建立專案才會有問題呢?因為我在 ASP.NET MVC 5 專案如何使用 C# 6.0 進行開發 文章中有提到,從 Visual Studio 2015 建立的 ASP.NET MVC 5 專案,預設就是用 C# 6.0 進行編譯,而 Visual Studio 2015 的 ASP.NET MVC 5 專案範本已經內建 Microsoft.CodeDom.Providers.DotNetCompilerPlatform 這個 NuGet 套件,而這個套件的用途是提供 Roslyn 編譯器服務,他可以幫助你 ASP.NET MVC 5 的 View 在執行時可以透過 Roslyn 進行動態編譯,也就是說裝了這個套件後,你就可以在 View 裡面使用 C# 6.0 的語法。
不過,由於 Roslyn 是一套全新的 C# 編譯器,他的編譯行為跟傳統的 C# 編譯器有一點點不太一樣,當你使用 Lambda 語法的時候,當你的 Lambda 語法有用到 Closure 特性,以我們的範例專案來說,在 View 裡面我們透過一個 for 迴圈產生多個欄位,而欄位中使用到的 @Html.TextBoxFor() 有傳入 i 變數,這個變數傳入 Lambda 之後會導致 C# 編譯器會自動產生一個匿名物件,也就是 CS$<>8__locals1 這個物件,而 ASP.NET MVC 5 的 ExpressionHelper 卻把編譯器動態產生的匿名物件名稱也給輸出了,導致這個 Bug 發生。
注意事項:物件或變數名稱 (identifier) 若出現兩個底線 ( __ ) 通常是編譯器或 Razor 這類動態產生程式碼工具會使用的名稱,你自己定義的變數名稱請盡量不要使用兩個底線當成變數名稱。
解決方案
我先來說明如何暫時解決這個問題,其實你只要透過 NuGet 套件管理員把 Microsoft.CodeDom.Providers.DotNetCompilerPlatform 套件移除,問題就可以迎刃而解,畢竟我們在 View 裡面其實不太需要用到 C# 6.0 語法,這是目前最快的解決方式。
另外一種方案,則是等待微軟推出修正版,目前 ASP.NET MVC 6 beta8 只後的版本已經確認修復了,但尚未發布的 ASP.NET MVC 5.2.4 版我從最新版原始碼來看 ( 看 master 分支 ),解決這個問題的程式碼還沒有被 commit 進去,所以還能難說在 ASP.NET MVC 5.2.4 是否會有修正版。
不過我自己 Fork 了一版 ASP.NET MVC 5 原始碼,並且參照 ASP.NET MVC 6 beta8 的修改方法 ( 原始碼 ) 將此問題修正了,我簡單測試過覺得應該是沒啥問題。我的做法是這樣的,先 Fork 一版 ASP.NET MVC 5 的原始碼,從 v3.2.3 標籤 (Tag) 建立一個 ExpressionHelperFixed 分支 (這裡的最新版就是我修正過的版本),並將相關程式碼修正,我只改了 src/System.Web.Mvc/ExpressionHelper.cs 這個檔案而已,修改的內容很簡單,其實就是把欄位名稱輸出過濾掉編譯器自動建立的物件名稱而已,修訂紀錄可參考這裡。
額外說明:由於 ASP.NET MVC 5 搭配的 Razor 版本是 v3 版本,所以在 ASP.NET MVC 5 原始碼專案上若看到 v3.2.3 的標籤,其實就代表這是 ASP.NET MVC 5.2.3 版本的意思,我也是摸索了好久才搞懂的,似乎沒看到有問題說明這個命名規則。
若要取得我的這個原始碼專案,只要連到我 Fork 的專案原始碼頁面,執行 git clone 將專案下載,並切換到 ExpressionHelperFixed 分支 ( git checkout ExpressionHelperFixed ) 即可取得原始碼。
這個專案是基於 ASP.NET MVC 5.2.3 去修改的,所以你只要在重新編譯之後將 System.Web.Mvc.dll 與 System.Web.Mvc.pdb 複製到你網站的 bin 目錄下,這個 Bug 就算解了。
雖然套用這段程式可以解決,我也只是 Coding for fun 而已,可以透過開源碼自己動手解決問題真的蠻過癮的。但我還是覺得還是移掉 Microsoft.CodeDom.Providers.DotNetCompilerPlatform 套件比較簡單啦,哈哈。
2018/07/14 補充:此問題已經在 ASP.NET MVC 5.2.5 版本修復,只要透過 NuGet 將 Microsoft.AspNet.Mvc 套件升級到最新版即可解決。
相關連結