我有一个MVC 3控制器动作定义如下:
public ActionResult _UpdateFilter(int a, string b, List<string> c)
效果很好。 有时还会向操作发送Dictionary<int, bool>
,这取决于用户在UI中的操作。
要填充该字典,我将隐藏字段添加到相应的表单中,如下所示:
<input type="hidden" id="id-@(something)-Key" name="sometimesSet[@(idx)].Key" value="@(myIntValue)" />
<input type="hidden" id="id-@(something)-Value" name="sometimesSet[@(idx)].Value" value="@(myBoolValue)" />
我像这样扩展了我的Controller Action:
public ActionResult _UpdateFilter(int a, string b, List<string> c, Dictionary<int, bool> sometimesSet = null)
到目前为止,这么好。只要我在表单中填充至少一个hidden
键/值对。如果我不这样做,我的Action就不会被调用,而且我得到例外:
Value cannot be null.
Parameter name: key
(问题末尾的长堆栈跟踪)。
问题
我收集路由映射无法确定sometimesSet
是可选的,但我不知道如何正确配置。我怎么做?我没有在Global.asax中编辑路径定义。
堆栈跟踪
at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
at System.Collections.Generic.Dictionary`2.ContainsKey(TKey key)
at System.Web.Mvc.ValueProviderUtil.GetKeysFromPrefix(IEnumerable`1 collection, String prefix)
at System.Web.Mvc.DictionaryValueProvider`1.GetKeysFromPrefix(String prefix)
at System.Web.Mvc.ValueProviderCollection.GetKeysFromPrefixFromProvider(IValueProvider provider, String prefix)
at System.Web.Mvc.ValueProviderCollection.<>c__DisplayClass11.<GetKeysFromPrefix>b__c(IValueProvider provider)
at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)
at System.Web.Mvc.ValueProviderCollection.GetKeysFromPrefix(String prefix)
at System.Web.Mvc.DefaultModelBinder.UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType)
at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass25.<BeginInvokeAction>b__1e(AsyncCallback asyncCallback, Object asyncState)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.BeginInvokeAction(ControllerContext controllerContext, String actionName, AsyncCallback callback, Object state)
at System.Web.Mvc.Controller.<>c__DisplayClass1d.<BeginExecuteCore>b__17(AsyncCallback asyncCallback, Object asyncState)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.Controller.BeginExecuteCore(AsyncCallback callback, Object state)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.Controller.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)
at System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)
at System.Web.Mvc.MvcHandler.<>c__DisplayClass6.<>c__DisplayClassb.<BeginProcessRequest>b__3(AsyncCallback asyncCallback, Object asyncState)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.MvcHandler.<>c__DisplayClass6.<BeginProcessRequest>b__2()
at System.Web.Mvc.SecurityUtil.<>c__DisplayClassb`1.<ProcessInApplicationTrust>b__a()
at System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(Action f)
at System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Action action)
at System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust[TResult](Func`1 func)
at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state)
at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state)
at System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
答案 0 :(得分:1)
我发现最干净的解决方案是实现我自己的ModelBinder,它理解如何填充Dictionary<K,V>
即使在线上找不到相关值(它将返回空Dictionary<K,V>
。
这是代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace My.ModelBinders
{
/// <summary>
/// Binds to a generic Dictionary using the basic format outlined at
/// http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
/// with the relaxation that the indices can be arbitrary integers (need not start at 0 or be sequential).
/// Returns an empty dictionary of no matching parameters found on the wire rather than throwing
/// an Exception as the current default binder does.
/// </summary>
/// <typeparam name="K">Key type</typeparam>
/// <typeparam name="V">Value type</typeparam>
public class OptionalDictionaryBinder<K,V> : CustomBinderBase, IModelBinder
{
/// <summary>
/// Pull key/value pairs out of request. Modified from
/// https://github.com/loune/MVCStuff/blob/master/Extensions/DefaultDictionaryBinder.cs
/// Files not currently supported.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private IEnumerable<KeyValuePair<string, string>> GetValueProviderData(ControllerContext context)
{
foreach (var fk in context.HttpContext.Request.Form.Keys)
{
yield return new KeyValuePair<string, string>((string)fk, context.HttpContext.Request.Form[(string)fk]);
}
foreach (var rd in context.RouteData.Values)
{
yield return new KeyValuePair<string, string>(rd.Key, (string)rd.Value);
}
foreach (var qp in context.HttpContext.Request.QueryString)
{
yield return new KeyValuePair<string, string>((string)qp, context.HttpContext.Request.QueryString[(string)qp]);
}
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
Dictionary<K, V> dict = new Dictionary<K, V>();
Dictionary<int, K> keys = new Dictionary<int, K>();
Dictionary<int, V> values = new Dictionary<int, V>();
foreach (KeyValuePair<string,string> formParam in GetValueProviderData(controllerContext))
{
if (formParam.Key.StartsWith(bindingContext.ModelName, StringComparison.InvariantCultureIgnoreCase))
{
int startbracket = formParam.Key.IndexOf("[");
if (startbracket != bindingContext.ModelName.Length) throw new Exception("Did not find [ directly after model name");
int endbracket = formParam.Key.IndexOf("]", bindingContext.ModelName.Length + 1);
if (endbracket == -1) throw new Exception("Did not find closing bracket in " + formParam);
int idx;
string idxText = formParam.Key.Substring(bindingContext.ModelName.Length + 1, endbracket - bindingContext.ModelName.Length - 1);
if (!int.TryParse(idxText, out idx))
{
throw new Exception("Could not parse numeric index from " + formParam);
}
if (formParam.Key.EndsWith(".Key", StringComparison.InvariantCultureIgnoreCase))
{
if (keys.ContainsKey(idx))
{
throw new Exception("The index " + idx + " was repeated.");
}
K dictKey;
try
{
dictKey = (K)Convert.ChangeType(formParam.Value, typeof(K));
keys.Add(idx, dictKey);
}
catch (NotSupportedException)
{
throw new Exception("The given key '" + formParam.Key + "' could not be converted to type K");
}
}
else if (formParam.Key.EndsWith(".Value", StringComparison.InvariantCultureIgnoreCase))
{
if (values.ContainsKey(idx))
{
throw new Exception("The index " + idx + " was repeated.");
}
V dictValue;
try
{
dictValue = (V)Convert.ChangeType(formParam.Value, typeof(V));
values.Add(idx, dictValue);
}
catch (NotSupportedException)
{
throw new Exception("The given value '" + formParam.Value + "' could not be converted to type V");
}
}
}
}
if (!keys.Keys.SequenceEqual(values.Keys))
{
throw new Exception("Keys and values do not match.");
}
foreach (KeyValuePair<int, K> kvp in keys)
{
dict.Add(kvp.Value, values[kvp.Key]);
}
return dict;
}
}
}
用法:
public ActionResult _UpdateFilter(int a, string b, List<string> c,
[ModelBinder(typeof(OptionalDictionaryBinder<int, bool>))]
Dictionary<int, bool> sometimesSet)