[MVC]自定义模型绑定器,从表单对模型进行赋值
一、奇葩的问题
之前自己造轮子的时候,遇到一个很奇怪的问题,虽然需求很奇葩,但是还是尝试解决了一下
当提交的表单里包含多个重复名称的字段的时候,例如
<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一样先让默认的处理,处理完再处理。而是需要全部都由开发人员手动处理。