IDictionary<,> 跟php的关联数组,或者 javascript 的 object,有着相似的作用。字典类型也算是很常用的基本数据结构,奇怪的是 .NET 虽然有这个数据结构,但真正用的时候会很现 MS 对它的支持实在是有限。Entity Framework 不支持映射,MVC 绑定也不完善!爹不疼娘不爱的赶脚啊。

假定我有一个 SortedDictionary<int, decimal> 类型的 MyDictionary 属性(这里只讨论简单的值类型,复杂的类型用 IEnumerable 去映射更合适)

如果我记得没错 php 绑定关联数据应该是这样就可以了:

1
2
3
4
5
6
7
<form action=“” method=“post”>
  <input name=“MyDictionary[5]” value=“400” />
  <input name=“MyDictionary[10]” value=“700” />
  <input name=“MyDictionary[20]” value=“1300” />
  <input name=“MyDictionary[40]” value=“2500” />
  <button>提交</button>
</form>

简洁明了。但是在 MVC 5抱歉这个无法正确绑定,这段优雅的代码只能绑定到 IDictionary<string, string> 的类型上!想要绑定上我们想要泛类型你只能用下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<form action=“” method=“post”>
  <input name=“MyDictionary.Index” value=“5” />
  <input name=“MyDictionary[5].Key” value=“5” />
  <input name=“MyDictionary[5].Value” value=“400” />
  <input name=“MyDictionary.Index” value=“10” />
  <input name=“MyDictionary[10].Key” value=“10” />
  <input name=“MyDictionary[10].Value” value=“700” />

  <input name=“MyDictionary.Index” value=“20” />
  <input name=“MyDictionary[20].Key” value=“20” />
  <input name=“MyDictionary[20].Value” value=“1300” />

  <input name=“MyDictionary[40].Index” value=“40” />
  <input name=“MyDictionary[40].Key” value=“40” />
  <input name=“MyDictionary[40].Value” value=“2500” />

  <button>提交</button>
</form>

又臭又长,代码足足多了3倍,看到眼睛都花掉!string 可以绑定但其它值类型不行真是相当“光腚肿菊”的设定。所以我们需要稍微修正下 DefaultModelBinder 的行为,让它回到正确轨道上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class YourBinder : DefaultModelBinder
{

  // 截停绑定事件
  public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
  {
    var modelType = bindingContext.ModelType;
    var dictionaryType = modelType.GetInterface("IDictionary`2");

    if (modelType.IsGenericType && dictionaryType != null)
    {
      // 特别对 IDictionary<,> 类型进行处理
      // 一般来讲客户端会提交所有的数据,最合适的猜测是重新创建一个新的实例替换掉原来,如果想要追加的行为去掉注释内的内容
      var model = /*bindingContext.Model ?? */this.CreateModel(controllerContext, bindingContext, modelType);
      var addMethod = dictionaryType.GetMethod("Add");
      var argTypes = dictionaryType.GetGenericArguments();
      var keyType = argTypes[0];
      var valueType = argTypes[1];

      IEnumerableValueProvider enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
      if (enumerableValueProvider != null)
      {
        IDictionary<string, string> keysFromPrefix = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName);
        foreach (KeyValuePair<string, string> current in keysFromPrefix)
        {
          try
          { // 转换失败的条目直接跳过,适用多数场景
            var key = Convert.ChangeType(current.Key, keyType);
            var value = Convert.ChangeType(bindingContext.ValueProvider.GetValue(current.Value).AttemptedValue, valueType);

            addMethod.Invoke(model, new[] { key, value });
          }
          catch(Exception ex)
          {

          }
        }
      }
      return model;
    }
    else // 非 IDictionary<,> 返还给 mvc 处理
    {
      return base.BindModel(controllerContext, bindingContext);
    }
  }
}

最后别忘了在 Global.axax 中 Application_Start 将这个新的Binder设为默认:

1
ModelBinders.Binders.DefaultBinder = new EmmolaBinder();