MVC 3 Controller Action中的可选字典参数

时间:2012-05-01 02:46:56

标签: asp.net-mvc-3

我有一个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)

1 个答案:

答案 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)