ASP.NET MVC Custom ValueProvider

透過Custom ValueProvider可以額外的處理一些資料格式

ASP.NET MVC Model Binding 提供了方常方便的機制,而且一般來說,能使用Model Binding 就應該盡量優先於其他方法(譬如FormCollection或是多個Parameter)

而 Default Model Binding 使用了 ValueProvider 來設定一個class 相對應的 property,例如 Product class,傳入一個controller 的Index method

 

	public class Product {		
		public String ProductID { get; set; }
		public String ProductName { get; set; }
		public Decimal Price { get; set; }
	}
public class ProductController : Controller {

		[HttpPost]
		public ActionResult Index(Product p) {
			this.ViewData.Model = p;
			return View();
		}
	}

line 4,Index method 內的參數 p(Product) 在建立時,其內的property (ProductID, ProductName, Price)的值依據 ValueProvider 來取得,而ValueProvider 由ValueProviderFactory 內的 GetValueProvider 取得,預設的ValueProviderFactory共有4個,存在ValueProviderFactories內,依序為

  1. FormValueProviderFactory
  2. RouteDataValueProviderFactory
  3. QueryStringValueProviderFactory
  4. HttpFileCollectionValueProviderFactory

為何說”依序”,因為在Binding 的機制,依據property name(例如ProductID),從這些ValueProviderFactory(4個)中以找到第1筆符合的資料為優先,詳細的內容不是這篇文章的主題,因此就先不談,不過看名稱應該也猜的出來這4個ValueProviderFactory 是負責處理哪些資料

再回歸主題,一個簡單的Product Edit 功能

Product class 做一些修改,增加Validation Attribute

	public class Product {

		[Required]
		public String ProductID { get; set; }

		[Required]
		public String ProductName { get; set; }

		[Required]
		[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:N0}")]
		public Decimal Price { get; set; }
	}

Controller : ProductController

	public class ProductController : Controller {

		public ActionResult Index() {
			ViewData.Model = new Product();
			return View();
		}

		[HttpPost]
		public ActionResult Index(Product p) {
			this.ViewData.Model = p;
			return View();
		}
	}


View : Index.aspx,使用client validation

<% Html.EnableClientValidation(); %>
	<% using(Html.BeginForm()){ %>		
		<%: Html.EditorForModel() %>
		<input type="submit" value="Submit" />
	<%} %>

 這時候會發生一個問題,就是Price如果有含千分位,例如1,234,在client validation是通過的,但在Model Binding 會發生問題

Snap2

原因是Price 是decimal type, 當傳入1,234 會出錯,這種問題有不同的解法,而此處利用Custom Value Provider來處理Price Property

Create 一個 PriceValueProviderFactory class, 繼承ValueProviderFactory,實作一個回傳IValueProvider 的 GetValueProvider method

public class PriceValueProviderFactory  : ValueProviderFactory{

		public override IValueProvider GetValueProvider(ControllerContext controllerContext) {
			return new PriceValueProvider(controllerContext.HttpContext);
		}

		class PriceValueProvider : IValueProvider {

			private HttpContextBase httpContext;

			public PriceValueProvider(HttpContextBase httpContext) {
				this.httpContext = httpContext;
			}

			#region IValueProvider 成員

			public bool ContainsPrefix(string prefix) {
				return prefix.Contains("Price");
			}

			public ValueProviderResult GetValue(string key) {
				if(!ContainsPrefix(key)) return null;

				string price = httpContext.Request.Form[key];
				return new ValueProviderResult(decimal.Parse(price), price, CultureInfo.CurrentCulture);
			}

			#endregion
		}
	}


 

再Create PriceValueProvider class 實作 IValueProvider interface,而PriceValueProvider 處理了”Price”的資料 (在此就不作嚴謹的Validation 了)

最後再將PriceValueProviderFactory 加入到ValueProviderFactories,但前面有提到有ValueProviderFactory 有序順的因素,因此將他加入到第一個,確保優先被Select 到

在global.asax的 Application_Start method 加入一行即可

ValueProviderFactories.Factories.Insert(0, new PriceValueProviderFactory());

 

既然是增加到ValueProviderFactories,同樣對 TryUpdateModel 一樣有效  

當然若只是單一上述的 Price 狀況,用Custom Value Provider ,可能過於overhead,但若是整個系統的Price 都有此問題考量,就可以考慮實作出特定的ValueProvider

例如系統UI 有關日期的input 必須以民國年輸入條件,但資料背後都是以西元年處理,這時在Custom ValueProvider 就處理掉民國年 <-> 西元年的工作