在某些ajax應用中,我們可能會用到如下的情境:
$.post('/Test/PostTest', { values: [1, 2, 3, 4] }, function(result){ //TODO:}, 'json' );
我們希望提交一個數組給伺服器。
於是我們建立了一個如下的Controller,來負責處理上面的ajax請求:
public class TestController : Controller{ [HttpPost] public JsonResult PostTest( int[] values ) { //TODO: return Json( new { success = true }); }}
可是當我們充滿期待的去測試我們剛才的代碼時,卻發現了一個問題。
值並沒有被正確的傳過來。
於是我們開啟了瀏覽器的開發人員工具,來看看到底jQuery提交了什麼內容給我們的伺服器。
我們發現,表單名稱被設定成為了 values[],而不是values。莫非是是mvc不能將values[]看成一個數組並自動轉化嗎?
於是我們開啟ILSpy,找到了System.Web.Mvc.FormValueProviderFactory的原始碼,並將它複製出來,作了一些擴充,以支援我們想要的功能。
public sealed class FormValueProviderFactoryEx : ValueProviderFactory { private readonly UnvalidatedRequestValuesAccessor _unvalidatedValuesAccessor; public FormValueProviderFactoryEx() : this(null) { } internal FormValueProviderFactoryEx(UnvalidatedRequestValuesAccessor unvalidatedValuesAccessor) { if (unvalidatedValuesAccessor == null) { unvalidatedValuesAccessor = ((ControllerContext cc) => new UnvalidatedRequestValuesWrapper(cc.HttpContext.Request.Unvalidated())); } this._unvalidatedValuesAccessor = unvalidatedValuesAccessor; } public override IValueProvider GetValueProvider(ControllerContext controllerContext) { if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); } return new FormValueProviderEx(controllerContext, this._unvalidatedValuesAccessor(controllerContext)); } }
下面這幾個是原來的FormValueProviderFactory用到的,但在System.Web.Mvc.dll中被聲明為internal,所以不得已複製了出來。
internal interface IUnvalidatedRequestValues{NameValueCollection Form { get; }NameValueCollection QueryString {get;}string this[string key]{ get; }} internal delegate IUnvalidatedRequestValues UnvalidatedRequestValuesAccessor(ControllerContext controllerContext); internal sealed class UnvalidatedRequestValuesWrapper : IUnvalidatedRequestValues { private readonly UnvalidatedRequestValues _unvalidatedValues; public NameValueCollection Form { get { return this._unvalidatedValues.Form; } } public NameValueCollection QueryString { get { return this._unvalidatedValues.QueryString; } } public string this[string key] { get { return this._unvalidatedValues[key]; } } public UnvalidatedRequestValuesWrapper(UnvalidatedRequestValues unvalidatedValues) { this._unvalidatedValues = unvalidatedValues; } }
下面是用於支援FormValueProviderFactoryEx的另外幾個對象的定義
public sealed class FormValueProviderEx : NameValueCollectionValueProvider { public FormValueProviderEx(ControllerContext controllerContext) : this(controllerContext, new UnvalidatedRequestValuesWrapper(controllerContext.HttpContext.Request.Unvalidated())) { } internal FormValueProviderEx(ControllerContext controllerContext, IUnvalidatedRequestValues unvalidatedValues) : base(controllerContext.HttpContext.Request.Form, unvalidatedValues.Form, CultureInfo.CurrentCulture) { } public override ValueProviderResult GetValue(string key, bool skipValidation) { var result = base.GetValue(key, skipValidation); if (result == null) { var subKeys = base.GetKeysFromPrefix(key); if (subKeys.Count > 0) { var firstItem = subKeys.First(); if (subKeys.Count == 1 && firstItem.Value == key + "[]") { return GetValue(firstItem.Value, skipValidation); } int n; if( int.TryParse(firstItem.Key, out n) ) { var indexList = new List<int>(subKeys.Count); if (subKeys.Keys.All(v => { if (int.TryParse(v, out n)) { indexList.Add(n); return true; } return false; })) { var arraySize = indexList.Max() + 1; var elements = new ValueProviderResult[arraySize]; foreach (var i in indexList) { elements[i] = GetValue(subKeys[i.ToString()]); } return new ArrayValueProviderResult(elements); } } var properties = new Dictionary<string, ValueProviderResult>(StringComparer.OrdinalIgnoreCase); foreach (var item in subKeys) { properties[item.Key] = GetValue(item.Value); } return new ObjectValueProviderResult(properties); } } return result; } } public class ArrayValueProviderResult : ValueProviderResult { private ValueProviderResult[] _Elements; public ArrayValueProviderResult(ValueProviderResult[] elements) { _Elements = elements; base.RawValue = elements.Select( v => v.RawValue ).ToArray(); base.AttemptedValue = "[" + string.Join(", ", elements.Select(v => v.AttemptedValue)) + "]"; } public override object ConvertTo(Type type, CultureInfo culture) { if (type.IsArray) { var elementType = type.GetElementType(); var array = Array.CreateInstance(elementType, _Elements.Length); int l = _Elements.Length; if (elementType == typeof(object)) { Array.Copy(_Elements, array, l); } else { for (int i = 0; i < l; i++) { var v = _Elements[i]; if (v != null) { try { array.SetValue(v.ConvertTo(elementType, culture), i); } catch { } } } } return array; } return null; } } public class ObjectValueProviderResult : ValueProviderResult { private IDictionary<string, ValueProviderResult> _Properties; public ObjectValueProviderResult(IDictionary<string, ValueProviderResult> properties) { _Properties = properties; base.RawValue = properties.ToDictionary(v => v.Key, v => v.Value.RawValue); base.AttemptedValue = "{" + string.Join(", ", properties.Select(v => string.Format("{0}: {1}", v.Key, v.Value.AttemptedValue ))) + "}"; } public override object ConvertTo(Type type, CultureInfo culture) { if (!type.IsPrimitive && !type.IsArray) { var constructor = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance).OrderBy(v => v.GetParameters().Length).FirstOrDefault(); if (constructor != null) { var args = constructor.GetParameters() .Where(v => !v.IsOptional) .Join(_Properties.DefaultIfEmpty(), v => v.Name, v => v.Key, (l, r) => r.Value).ToArray(); var obj = Activator.CreateInstance(type, args); foreach( var property in type.GetProperties( BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty )) { if (property.GetIndexParameters().Length > 0) continue; ValueProviderResult propertyValue; if (_Properties.TryGetValue(property.Name, out propertyValue) && propertyValue != null ) { try { if (property.PropertyType == typeof(object)) { property.SetValue(obj, propertyValue.RawValue, null); } else { property.SetValue(obj, propertyValue.ConvertTo(property.PropertyType, culture), null); } } catch { } } } return obj; } } return null; } }
在做完上面的事情之後,我們就可以考慮把FormValueProviderFactory替換成為FormValueProviderFactoryEx了。
於是我們在Application_Start中,添加如下的代碼:
for (int i = 0; i < ValueProviderFactories.Factories.Count; i++) { if (ValueProviderFactories.Factories[i] is FormValueProviderFactory) { ValueProviderFactories.Factories[i] = new FormValueProviderFactoryEx(); break; } }
現在我們再來測試之前的代碼:
很高興的看到,我們的值,已經正確的解析出來了!
我們再來測試傳遞一個對象:
$.post('/Test/PostTest', { obj: { Id: 1, Values: ['aa', 'bb', 'cc']} }, function (result) { //TODO: }, 'json');
我們把Action的代碼也稍作修改:
[HttpPost] public JsonResult PostTest( MyObject obj ) { //TODO: return Json( new { success = true }); }
MyObject的定義如下:
public class MyObject { public int Id { get; set; } public string[] Values { get; set; } }
如預想中的一樣,我們得到了下面的結果:
OK,大功告成。
得於某種目的,上面的代碼中有兩處需要說明一下:
ArrayValueProviderResult 類中的
if (elementType == typeof(object))
{
Array.Copy(_Elements, array, l);
}
當數群組類型為object[]時,複製把原始的ValueProviderResult過去了,這裡看個人需要可自己修改。
PS:只是很膚淺的實現了這種直接使用jQuery來提交對象給asp.net mvc的支援,代碼未做最佳化,未作任何合理性的設計。
如果有需要的猴子,可以參考自己實現一個。