[转载]让asp.net mvc的Action支持jQuery直接提交的javascript对象 – 路人已 – 博客园.
在某些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(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(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 _Properties; public ObjectValueProviderResult(IDictionaryproperties) { _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的支持,代码未做优化,未作任何合理性的设计。
如果有需要的猴子,可以参考自己实现一个。