一、奇葩的問題
之前自己造輪子的時候,遇到一個很奇怪的問題,雖然需求很奇葩,但是還是嘗試解決了一下
當提交的表單里包含多個重復名稱的字段的時候,例如

<form action="/Test/save" method="post"> <!--省略其他字段--> <input type="text" name="value" /> <input type="text" name="value" /> <input type="text" name="value" /> <!--有可能有更多的name為value的input--> <input type="submit" value="submit" /> </form>
如果需要模型在Action進行接收,那么通常的解決方案是用一個 IEnumerable<T> 類型或其派生類型來接收數據,以保證數據的完成性,例如這樣一個模型

public class Test { //省略其他字段 public string value { get; set; } }
一般來講這么做沒啥問題,可是問題來了
如果我需要將結果以逗號(,)分割並輸出,那么我就需要寫這樣一行代碼 string.Join(",", model.Test); ,無論是在哪里。
如果不在模型字段上做字符串拼接操作的,還會導致這個模型無法復用。而且代碼看起來很不優雅。
二、思考問題
我把這個問題也發到了博問上.Net MVC 模型接收參數問題
普遍得到的答案都是我上面說到的解決方式,似乎不是我想要的。
嘗試思考一下。
標准的Http的Request接收到的時候,對與MVC來說他只是一個Form或者QueryString(如果理解有誤,歡迎指正),那么MVC框架是怎么做到將一個表單綁定到一個模型上呢?
為什么 HttpContext.Request.Form 中對應的字段倒是完成了字符串拼接的?
為什么表單和模型的賦值不一樣?表單里的值我能不能用?
帶着問題猛戳了一番度娘后,發現了一個驚喜的東西,模型綁定器(ModelBinder)
概念我就不貼了,見這個大神的博客 ASP.NET MVC5 ModelBinder
這一篇是Core的, ASP.NET Core MVC Model Binding: Custom Binders
三、解決方案
1、建立一個自己的模型綁定器
Framework:
public class TestModelBinder : DefaultModelBinder, IModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var model = base.BindModel(controllerContext, bindingContext); //do something for model to format from Form or other place return model; } }
Core:
public class TestModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { var model = bindingContext.Model; //do something for model to format from Form or other place bindingContext.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } }
只要在注釋的地方,對模型需要賦值的字段進行操作就可以了
PS:具體操作就仁者見仁智者見智了,簡單說一下我自己的做法,反射遍歷模型類型為string的字段,如果模型字段值與表單同名的值不一致,由表單從新給字段賦值。
這個做法的效率有待提升,不過目前先這么解決。
好了模型綁定器有了,不過現在還不能用。
2、定義模型綁定器的Provider
這個方法在上面那個大神的博客里有寫,我在這里再啰嗦一下
Framework:
public class TestModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(Type modelType) { if (modelType == typeof(Test)) { return new TestModelBinder(); } return null; } }
這里傳入的Type是模型本身的Type,用來過濾對拿一些模型生效,一般會使用默認綁定器 DefaultModelBinder 的。
Core:
public class TestModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context.Metadata.ModelType == typeof(DateTime)) { return new TestModelBinder(); } return null; } }
除了參數不太一樣,兩個版本基本長得一毛一樣。
3、全局注冊
定義為全局的模型綁定器就可以在所有模型綁定的時候啟動。無需再加任何其他東西,但是同時面臨一個問題就是使用原生的綁定器需要在參數上單獨聲明。
Framework是在Global.asax里面
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ModelBinderProviders.BinderProviders.Insert(0, new TestModelBinderProvider()); //或者下面這個也可以,二選一 //ModelBinders.Binders.Add(typeof(Test),new TestModelBinder()); } }
Core是在Startup.cs里
public void ConfigureServices(IServiceCollection services) { services.AddMvc(option => { option.ModelBinderProviders.Insert(0, new TestModelBinderProvider()); })
4、定義模型綁定屬性
在特殊的綁定器不多的情況下我們可以選擇將綁定器定義為屬性
Framework:
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] public class FormFormatAttribute : CustomModelBinderAttribute { public override IModelBinder GetBinder() { return new TestModelBinder(); } }
Core:
[AttributeUsage(AttributeTargets.Parameter,AllowMultiple = false, Inherited = false)] public class TestModelBinderAttribute : ModelBinderAttribute { public TestModelBinderAttribute() : base(typeof(TestModelBinder)) { } }
看起來基本也是一毛一樣
屬性的調用方法兩個是一致的,在參數前面加上 [TestModelBinder] 就可以了。
總結:
經過一番折騰,模型綁定器搞定了,也能更加優雅綁定變量了。也不需要那么一堆什么拼接字符串啊,額外屬性啊,之類的。
但是需要注意一點,Core的綁定器沒有默認基類(也可能是我沒找到,如果找到了,如果有找到的麻煩歡迎分享),所以不能像Framework一樣先讓默認的處理,處理完再處理。而是需要全部都由開發人員手動處理。