動態修改 View Engine 的搜尋順序

應該有寫 ASP.NET MVC 都看過這個錯誤畫面,其實這邊就是預設的搜尋順序啦,如果找不到的時候就會出現下面的畫面。這個是 Home/Index 執行時候的畫面,預設動作會先找尋 WebPage View 再找尋 Razor (MVC 3 Later) View ,優先順序是 Views/{Controller}/{Action} > Views/Shared/{Action} 。
image
今天剛好遇到一個多語系支援的需求,但是又不是每一個頁面都支援全部的語系,因為內容頁面有些是另外由人工翻譯完才加入,希望在還沒翻譯完之前先顯示原文,可以根據直接加入檔案新增語言,所以我的想法先找語系資料夾下面的 View ,如果沒有的話再用原本的搜尋方式尋找。
image
如果在不同語系有另外設定的 View 就針對語系先套用,沒有的話就是預設的輸出了,這樣感覺很方便,而且假如說原本頁面的英文版本還沒翻譯好,也可以等翻譯完再加入,還沒加入之前瀏覽也不會也掛掉的問題。但是要這樣做預設的功能沒有支援,所以必須修改 View Engine 尋找 View 的順序,讓他在找尋的時候先根據語系尋找。

建立自訂 View Engine 繼承 RazorViewEngine

public class MultiLanguegeRazorViewEngine : RazorViewEngine

這個地方因為我只有用到 Razor 的 View 所以就只需要一個自訂的 View Engine 繼承 RazorViewEngign,如果有用到 Aspx 或是 ascx 的 View 就是需要另外一個 View Engine 繼承 WebFormViewEngine。最後在依照想要哪個View Engine 先做找尋的動作加入 ViewEngines。

修改 View Engine Formats 屬性

  • AreaMasterLocationFormats (找尋 Area 裡面的 MasterPage)
  • AreaPartialViewLocationFormats (找尋 Area 裡面的 ParttialView)
  • AreaViewLocationFormats (找尋 Area 裡面的 View)
  • MasterLocationFormats (找尋 MasterPage )
  • PartialViewLocationFormats (找尋 ParticalView)
  • ViewLocationFormats (找尋 View)

View Engine 有這幾個 Format 屬性,在建構子的時候修改,在不同情況下會根據不同 Format 做找尋的動作。如果有需要修改的 Format 在額外去做設定,不然就會以繼承的為準。

public MultiLanguegeRazorViewEngine()
: base()
{
ViewLocationFormats = new[] {
"~/Views/{1}/%1/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/%1/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
};

PartialViewLocationFormats = new[] {
"~/Views/{1}/%1/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/%1/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
};
}

Override 原本的方法

在搜尋 View 的時候會去呼叫 ViewEngine 裡面的這幾種方法,在呼叫的時候把剛剛加入 Formats 裡面的 “%1” 替換成想要的位置,會在這邊作處理是因為 controller 處理完之後再呼叫 View 的時候會將 controllerContext 參數傳進來,這樣就可以從傳進來的 controllerContext 作為判斷依據來動態改變 View 的搜尋順序。

protected override 
IView CreatePartialView(ControllerContext controllerContext,
string partialPath)

{

string Lang = controllerContext.Controller.ViewBag.Lang;

return base.CreatePartialView(controllerContext, partialPath.Replace("%1", Lang));
}



protected override
IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)

{

string Lang = controllerContext.Controller.ViewBag.Lang;

return base.CreateView(controllerContext, viewPath.Replace("%1", Lang), masterPath.Replace("%1", Lang));
}



protected override bool FileExists(
ControllerContext controllerContext, string virtualPath)

{

string Lang = controllerContext.Controller.ViewBag.Lang;

return base.FileExists(controllerContext, virtualPath.Replace("%1", Lang));
}

在 Global 修改 View Engine 成自訂的 View Engine

最後在Application_Start()的地方先清除預設的ViewEngines,在重新加入剛剛設定的MultiLanguegeRazorViewEngine。

protected void Application_Start()
{

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new
MultiLanguegeRazorViewEngine());
}

這樣就大功告成了,ViewEngine 搜尋 View 的邏輯就會根據自訂的路徑順序來做搜尋的動作。

P.S.

Formats 參數設定是

  • {0}: ActionName
  • {1}:ControllerName
  • {2}:AreaName

有用到 AreaName 的地方就只有 Area 相關的 Formats,不是每個 Formats 都需要。


http://weblogs.asp.net/imranbaloch/archive/2011/06/27/view-engine-with-dynamic-view-location.aspx
Custom ViewEngine ASP.NET MVC 3 – Stack Overflow
Creating your own MVC View Engine For MVC Application

MVC4 多了什麼新東西 – Single Page Application (SPA)

ASP.NET MVC4 Beta 除了 Web API 也同樣新增了另一個專案範本,Single Page Application (SPA),為什麼會有只要一頁的專案,那應該只是拿來測試的吧,有必要特地新增一個專案範本嗎。
image
原來這邊的 Single Page 指的是用單一頁面完成整個網站的瀏覽,利用 javascript 直接建立起 Client 的系統,再透過 javascript 來處理 Request & Response ,好處是可以讓網頁動作更流暢.也可以結合 Web API 的概念,用同樣的 controller 同時對不同 Client 支援(當然 client 還是要分好幾套來寫),這樣想起來其實 SPA 根本就應該跟 Web API 放一起就好了啊。
spa.clientstack
建立的專案其實重點就是在 javascript 上面,這邊 .net 跟 Visual Studio 能提供的支援就少了很多了,但是我覺得將來漸漸這種類型的網站會愈來愈多,更重視在使用者互動上。
image


Steve Sanderson’s Knockout Blog

MVC4 多了什麼新東西 – Web API

image
Microsoft 在前幾天發表了 ASP.NET MVC4 Beta,長久以來 .net 在 Layout 方面都顯得較為弱勢,畢竟系統邏輯才是 Microsoft 的強項,從 ASP.NET WebPage > ASP.NET MVC > ASP.NET Web API 都一直在做著輕量化的工程,讓整個 Framework 不要去包裝太多。
Web API 這個 Framework 結合了 ASP.NET MVC 還有 WCF 的概念而成,讓 Web API 的 Service 變得更 RESTful 。也是 Microsoft 放手讓 .net developer 可以更加便利選擇 Client 端的技術,Web 的 jQuery , Extjs , Flash, iOS 的 Cocoa , Android Java , WindowsPhone Sliverlight …..

建立 Web API 專案

在安裝完 ASP.NET MVC4 Beta 之後,建立新的 ASP.NET MVC 4 Web Application
image
在選單中可以發現多了一個新的 Template – Web API
image
建立之後的檔案結構目錄依然是保持 ASP.NET MVC 的樣子,但是可以發現多了一個 ValueController ,這個就是 Web API 的重點了。
image
ValueController 裡面的程式碼,改由繼承 ApiController ,Method 名稱就直接對應 HttpMethod

public class ValuesController : ApiController{
// GET /api/values
public IEnumerable Get()
{
return new string[] { "value1", "value2" };
}

// GET /api/values/5
public string Get(int id)
{
return "value";
}

// POST /api/values
public void Post(string value)
{
}

// PUT /api/values/5
public void Put(int id, string value)
{
}

// DELETE /api/values/5
public void Delete(int id)
{
}
}

另外在 Global.asax Route 也可以看到多註冊了一組

routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

利用 Chrome REST Console 測試

image

image

回傳的結果

image

也支援 Xml

image


Tutorial: Your First Web API

Video: Your First Web API

Building HTTP services with ASP.NET Web API in MVC 4 Beta

Chrome 擴充功能 – REST Console

ModelStateValue 常遇到的同名衝突

ASP.NET MVC Model Binder 在 mvc 中處理掉所有 Client Side 跟 Server Side 的對應,處理掉很多繁瑣的小細節,寫起來變得非常的直覺。但是就是因為太直覺了,讓人往往忘記它的存在,Model Binder 發生的問題也是非常容易鬼打牆的地方。遇到這種問題我之前也是非常相信直覺的判斷,但是事實證明不能太鐵齒,遇到問題還是趕快把 .pdb 加進來一起偵錯吧
我個人很喜歡在 View 裡面用強型別 Model 配合 HTml.Helper 來做輸出的動作,不過有時候會遇到 Helper 輸出結果跟預設的想像不同的狀況。預設的想法就是 Helper 應該會輸出 Model 當下狀態的結果,不過反覆檢查下才知道 Helper 動作並不是如此。

ModelStateValue 的同名衝突

Html.Helper 在強型別配合下並不是直接輸出 Model 裡面的值,他一樣會同時參考其他資料來源,所以當使用 HtmlHelper 的時候,會容易因為呼叫的方式而預想成是直接根據強型別 Model 來作輸出。

Binding 進 ModelStateValue 裡面的資料

ModelState 在外部存取的時候是 readOnly 所以沒有辦法直接加入值,最常遇到的狀況就是在 get 或 post 的 Data 資料被 binding 進去 ModelStateValue 裡面,為了重現這個狀況
首先我建立一個當作強型別的 Model

public class HomeIndexModel{
public string Phone { get; set; }
}
相關的 Controller
public ActionResult Index(HomeIndexModel model)
{
model.Phone = "0";
return View(model);
}
View
@model ModelStateValue.Models.HomeIndexModel<section class="features">
@using (Html.BeginForm())
{
@Html.TextBoxFor(it => it.Phone)
<input type="submit" value="submit" />
}
</section>

首先在一開始進入頁面的時候,helper 顯示出來是 controller 設定的 0。

image

之後輸入其他值並且 Submit 回 Server Side。

image

Model 的值也確實被改變了。

image

但是之後回傳頁面卻還是 Post 回傳的值。

image

這個問題當初確實困擾我一陣子,當然有很多方法可以避免,但是跟我原本對 MVC 的想法有一些衝突,其實也就是對整個 Framework 不夠熟悉才會這樣。雖然已經被設置為強型別的 View ,但是 Html Helper 的資料來源並不是強型別 Model 而是 ModelState

image

而因為 Post 回 Server 的時候 PostData 已經綁定進入 ModelState 所以在輸出的時候抓到的並不是強型別 Model 的值,而是一開始被 binding 的部分。


http://stackoverflow.com/questions/9529730/view-does-not-affect-models-changes

ASP.NET MVC 開發心得分享 (6):小心使用 FormCollection

ASP.NET MVC 開發心得分享 (4):微調 Model Binder 屬性

ASP.NET MVC 開發心得分享 (12):Model Binder 的陷阱

設定 Visual Studio 偵錯 Microsoft .NET 元件

之前想要對 ASP.NET MVC Framework 元件做偵錯的時候我都是下載原始碼專案,然後把參考的部分改掉,還要額外設定 Config 的部分(我都是參考這篇)。後來才發現,如果只是想要看到逐步偵錯的程式碼,其實可以不必要這麼麻煩,微軟的部分元件有開放 .pdb 可以看看程式碼到底了些什麼事,到底是元件的 bug 還是根本就是使用的方式錯了。

Visual Studio 2010 > Debug > Options and Settings > Debugging > General

  1. 取消 Enable Just My Code
  2. 勾選 Enable Source Server Support

image

Visual Studio 2010 > Debug > Options and Settings > Debugging > Symbols

image

在這邊我只想要設定 System.Web.Mvc.DLL 的話,就選擇 Only specifieds modules > Specify Modules

image

按下確定之後就會從 Server 上面下載可用的 .pdf 。也會出現要你同意 License 的部分

image

之後一樣設定中斷點在想進入的地方,如果要看 Htm.TextBoxFor 到底幫你做了些什麼

image

一樣 F11 就可以進入逐步偵錯了

image

不過比起加入原始碼參考還是有些不足的地方,尋找至定義在這裡就沒辦法使用囉,如果要看更詳細的資料還是下載原始碼吧,或是另外開起來當作手動參考,就可以不用修改 Web.Config 那些設定,也可以直接 Release 發佈。

image


Stepping into ASP.NET MVC source code with Visual Studio debugger
Setting up Visual Studio 2010 to step into Microsoft .NET Source

Address Editor for ASP.NET MVC3 – Format the Address String by Google Map

image
輸入地址
image
自動查詢 google map 顯示查詢結果,將地址字串轉為 api 回應的結果

[Required]
[Display(Name = "地址")]
[UIHint("MvcAddressEditor")]
public string Address { get; set; }

利用 UIHint 自動套入 Template,另外新增一個 MvcAddressEditor.cshtml

@model String
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
$(function () {
var geocoder;
var map;
$(document).ready(function () {
geocoder = new google.maps.Geocoder();
//default location
//可以新增查詢當地位置當成預設值
//http://code.google.com/intl/zh-TW/apis/maps/documentation/javascript/basics.html
var latlng = new google.maps.LatLng(40.69847032728747, -73.9514422416687);
var myOptions = {
zoom: 12,
center: latlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
//first show map
map = new google.maps.Map($("#" + "@(Html.ClientIdFor(p => p) + "-google-map-display")")[0], myOptions);
});

$("#" + "@(Html.ClientIdFor(p => p))").live("change", function () {
var address = $(this).val();
geocoder = new google.maps.Geocoder();
if (geocoder) {
geocoder.geocode({ 'address': address }, function (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results[0].types[0] != "street_address") {
//the result address type
//alert("Please enter more detailed");
}
map.setCenter(results[0].geometry.location);
$("#"+"@(Html.ClientIdFor(p => p))").val(results[0].formatted_address);
var marker = new google.maps.Marker({
map: map,
position: results[0].geometry.location
});
}
else {
alert("Geocode was not successful for the following reason: " + status);
}
});
}
});
});
</script>
@Html.TextBoxFor(it => Model, new { Style = "width: 500px;" })
<div id='@(Html.ClientIdFor(p => p) + "-google-map-display")' style="width: 300px; height: 300px">
</div>

可以從這裡下載 Download 用的是 Google Maps Javascript API 第 3 版

Reference

Google Maps Javascript API 第 3 版服務

ClientIdFor Mvc Helper

ASP.NET MVC – AllowHtml & FormCollection 發生的問題

[AllowHtml]
public string html { get; set; }

前幾天遇到一個問題,在 edit 頁面 post 的時候,一直遇到說安全性的問題,回傳具有危險性的字串而造成錯誤,的確回傳的欄位中有一個是具有 Html 的內容,但是已經在 Model 部分加上 [AllowHtml] 的屬性,而且同樣一個 Model 被不同的 Action 一起使用。確實在加上[AllowHtml] 屬性之後是可以回傳 html 的內容,但是錯誤訊息也指向是 html 內容回傳的錯誤。

具有潛在危險 Request.Form 的值已從用戶端 (html="<a></a>") 偵測到。 
描述: 要求驗證偵測到具有潛在危險的用戶端輸入值,對這個要求的處理已經中止。
這個值可能表示有人嘗試危害應用程式的安全性,例如跨站台的指令碼處理攻擊。若要
允許頁面覆寫應用程式要求驗證設定,請將 httpRuntime 組態區段中的 requestV
alidationMode 屬性設定為 requestValidationMode="2.0"。
例如: <httpRun
time requestValidationMode="2.0" />。
設定這項值之後,您就可以停用要求驗
證,方法是在頁面指令或 <pages>
組態區段中設定 validateRequest="false"。但
是我們強烈建議您的應用程式應該明確地檢查所有這類的輸入。
如需詳細資訊,請參閱
http://go.microsoft.com/fwlink/?LinkId=153133。

找了一下子才發現原因出在 FormCollection 這個參數上,在 Post 的時候,除了接收 Model 的部分,還多接收了一個 FormCollection 的參數

[HttpPost]
public ActionResult Edit(ItemModel model, FormCollection collection)

但是 FormCollection 並不會受到 Model 屬性的影響,即使在欄位屬性加上了 [AllowHtml] ,FormCollection 在判斷到有 Html 回傳的時候,還是判斷具有危險。

Reference

http://stackoverflow.com/questions/5022134/mvc-3-rtm-allowhtml-doesnt-work-when-using-formcollection

http://www.dotblogs.com.tw/johnny/archive/2010/03/01/13829.aspx

ASP.NET MVC3 Action Filter 控制輸出的 Layout

在開發 Mvc 的時候可能會用到 RenderAction 的方式去將其他 Action 輸出的結果一次回傳,RenderAction 回傳的結果會是比較簡單的,模組化之後的回傳。但是也有可能原本的 Action 已經是完整的頁面,但是可能為了需要過場特效或是各種原因她必須在某些狀況被包含在 Render 輸出。
原本預設的 Attribute 就有包含 [ChildActionOnly] 可以控制只能被包含在其他的回傳之中,配合 PartialView() 可以很容易做到單純子項目的輸出,但是今天想要的是可以把頁面回傳或是RenderAction 的回傳都一併處理掉。


public class ChildActionWithoutLayoutAttribute
: ActionFilterAttribute, IActionFilter, IResultFilter
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.IsChildAction)
{
try
{
((ViewResult)filterContext.Result).MasterName =
"~/Views/Shared/_Partial.cshtml";
}
catch
{
}
}
base.OnActionExecuted(filterContext);
}
}

利用 IsChildAction 的判斷可以在 OnActionExecuted 的時候去指定不同的 Master Page,要注意的是如果有在各個頁面中去各自指定的情況下,而不是統一由 _ViewStart.cshtml 統一指定的話,這個的 OnActionExecuted 必須改為 OnResultExecuted 才不會被頁面上的設置蓋掉。執行順序是 _ViewStart.cshtml > OnActionExecuted > View > OnResultExecuted

@{
Layout = "~/Views/Shared/_Layout.cshtml";
}

▲如果有在各自的 view 去額外設定的話,則要將 Action Filter 的事件延後
另外一個要注意的是,在這邊的 MasterName 設定為空字串或是 null 是沒有效果的,如果找不到值就會帶入預設的 Layout ,所以為了達到這個效果額外開了一個 _Partial.cshtml 裡面只有 @RenderBody() 一行。

如果要判斷是不是 Ajax 的請求也可以用  filterContext.HttpContext.Request.IsAjaxRequest() 來判斷。

ASP.NET MVC – MaxLength & MinLength Client 驗證

在 Mvc 的 Model 屬性中有 MaxLength 跟 MinLength 的限制屬性,一般情況像是 [Required] 必填還有 Regex 的限制都可以搭配 jquery.validate 做到 Client 的 javascript 驗證效果。我本來以為 MaxLength 跟 MinLength 應該也可以達到同樣的效果在 Javascript 先行驗證。

image

▲在 Model 加上長度限制

image

▲可是在 View 使用 TextboxFor 產出的 Html 並沒有加入 jquery.validate 的屬性

原本想說會不會是內建的方法沒有支援,但是怎麼想都覺得不太可能,這麼常用的方法,不支援也太不合常理了吧。爬了網路上的文章才發現如果要限制字串的長度的話,需要用 [StringLength] Attribute,才會在 TextboxFor 的時候自動加入 jquery.validate 的屬性。

image

▲需要用 [StringLength] Attribute

image

▲加入了 jquery.validate 需要的屬性

image

▲[StringLength] Attribute 也提供了最小長度的具名參數

image

▲Html Helper 產出的 Html

Reference

MaxLength Attribute not generating client-side validation attributes

http://stackoverflow.com/questions/6801656/maxlength-attribute-not-generating-client-side-validation-attributes

ASP.NET C# MVC ViewModel with valueinjecter

Introduction

Model 跟 ViewModel 這塊之前一直沒有發現比較好的方式去處理,Model 跟 ViewModel 在設計的時候通常都是會有一些關聯,但是又希望他們可以獨立運作不要關聯,真的需要用到他們的關係又可以很方便的去使用。在 presentation layer 跟 Domain model 應該是要可以分離的很徹底,又可以準確的被轉換。

image

可以看到在 Service 勢必要將 ViewModel 跟 Model 做一個轉換才能繼續接下來的動作,雖然這是兩個不同的東西,但是大多數情況下,他們之間還是會有斷不掉的羈絆,最近接了同事的案子用到了一個工具Value Injecter

Value Injecter

image

▲直接在 NuGet 上面就可以找到了

這個東西可以幹嘛呢,就是幫你把物件的值注入到另一個物件中,兩個物件可以完全獨立不用任何參考。

Implement

這邊主要的目的只是要建立一個訂單服務,傳入訂單資料,然後到資料庫去建立一份訂單起來。

image

▲Service 的建立訂單,看得出來這邊只接受 Domain.Order ,不接受 ViewModel 的型態,所以這邊需要的就是將 IOrderModel 轉變成可用的 Domain.Order。

image

▲終於輪到 ValueInjecter 出場了,在這邊將 ViewModel 傳入的資料注入我們需要的物件。

image

▲再來我們直接用Cart 這個 ViewModel 來呼叫 Service 的 Submit method

image

▲Injecter 的結果,直接的屬性可以被注入,可是再下層的 OrderDetails 卻沒有帶回應該有的部分。

在來改寫一下 inject 的部分,要完成 Inject 的動作必須要達成兩個條件

1.型態完全相同

2.命名完全一樣(包括大小寫)

image

▲InjectFrom 的時候可以自訂加入要一起作注入的物件,在這個時候先到底下關聯的部分先做轉換,所以這邊OrderDetails 接到的會是完全正確的 ICollection<OrderDetail> OrderDetails ,符合命名跟型態兩個條件。

image

▲將相關聯的部分也一併轉入

Reference

Value Injecter – object(s) to -> object mapper

http://valueinjecter.codeplex.com/releases/view/60311

快速搬移物件內的資料至其他型別 – .NET ValueInjecter

http://kelp.phate.org/2011/08/net-valueinjecter.html