现在的位置: 首页 > 综合 > 正文

MVC学习笔记六:模型绑定【下】

2017年10月09日 ⁄ 综合 ⁄ 共 8773字 ⁄ 字号 评论关闭

模型绑定

一.手工调用模型绑定

使用前面演示的绑定字典的一个示例:
将方法参数去掉:
        public ActionResult ModelBindSheep()
        {
            Dictionary<string, Sheep> sheeps=null;
            if (sheeps == null)
            {
                sheeps = new Dictionary<string, Sheep>();
                sheeps.Add("firstSheep", new Sheep { Name="Tom" , AddInfo=new AdditionalInformation{Country="China",City="ShangHai"}});
                sheeps.Add("secondSheep", new Sheep { Name = "Jack", AddInfo = new AdditionalInformation { Country = "America", City = "sanfrancisco" }});
                sheeps.Add("thirdSheep", new Sheep { Name = "Jerry", AddInfo = new AdditionalInformation { Country = "France", City = "London" }});
            }

            return View(sheeps);
        }

前台代码改为:

@using MvcModelBind.Models;
@model Dictionary<string, MvcModelBind.Models.Sheep>
@{
    ViewBag.Title = "ModelBindSheep";
    int i = 0;
}

<h2>绑定到字典</h2>

@{

    ViewData["[0].value.Name"] = Model["firstSheep"].Name;
    ViewData["[0].value.AddInfo.Country"] = Model["firstSheep"].AddInfo.Country;
    ViewData["[0].value.AddInfo.City"] = Model["firstSheep"].AddInfo.City;

    ViewData["[1].value.Name"] = Model["secondSheep"].Name;
    ViewData["[1].value.AddInfo.Country"] = Model["secondSheep"].AddInfo.Country;
    ViewData["[1].value.AddInfo.City"] = Model["secondSheep"].AddInfo.City;

    ViewData["[2].value.Name"] = Model["thirdSheep"].Name;
    ViewData["[2].value.AddInfo.Country"] = Model["thirdSheep"].AddInfo.Country;
    ViewData["[2].value.AddInfo.City"] = Model["thirdSheep"].AddInfo.City;
}

@using (Html.BeginForm("ModelBindSheep", "Sheep"))
{ 
    
    <input type="hidden" name="[0].key" value="firstSheep" />
    @:第 @(i + 1) 只羊的名字: @Html.Editor("[0].value.Name")<br />
    @:第 @(i + 1) 只羊的国家: @Html.Editor("[0].value.AddInfo.Country")<br />
    @:第 @(++i) 只羊的城市: @Html.Editor("[0].value.AddInfo.City")

    <p>----------------------------------------------------</p>
    <input type="hidden" name="[1].key" value="secondSheep" />
    @:第 @(i + 1) 只羊的名字: @Html.Editor("[1].value.Name")<br />
    @:第 @(i + 1) 只羊的国家: @Html.Editor("[1].value.AddInfo.Country")<br />
    @:第 @(++i) 只羊的城市: @Html.Editor("[1].value.AddInfo.City")

    <p>----------------------------------------------------</p>
    <input type="hidden" name="[2].key" value="thirdSheep" />
    @:第 @(i + 1) 只羊的名字: @Html.Editor("[2].value.Name")<br />
    @:第 @(i + 1) 只羊的国家: @Html.Editor("[2].value.AddInfo.Country")<br />
    @:第 @(++i) 只羊的城市: @Html.Editor("[2].value.AddInfo.City")<br />

    <input type="submit" name="btn" value="提交" />
}

编译运行。修改TextBox框的值,可以发现,修改完成,点击提交按钮之后,TextBox数据又恢复原样了(如下图)。可见根本就没发生模型绑定。


那怎样才能不添加动作方法参数,又要执行模型绑定呢?只要在方法中加一句话就可以:
  public ActionResult ModelBindSheep()
        {
            Dictionary<string, Sheep> sheeps=null;
            if (sheeps == null)
            {
                sheeps = new Dictionary<string, Sheep>();
                sheeps.Add("firstSheep", new Sheep { Name="Tom" , AddInfo=new AdditionalInformation{Country="China",City="ShangHai"}});
                sheeps.Add("secondSheep", new Sheep { Name = "Jack", AddInfo = new AdditionalInformation { Country = "America", City = "sanfrancisco" }});
                sheeps.Add("thirdSheep", new Sheep { Name = "Jerry", AddInfo = new AdditionalInformation { Country = "France", City = "London" }});
            }

            UpdateModel(sheeps);//就是这一句了

            return View(sheeps);
        }

编译运行。修改TextBox框的值,修改完成,点击提交按钮之后,TextBox数据随之变化,即发生模型绑定了~


UpdateModel这个方法很有很有意思,它将模型绑定过来的对象覆盖到当前对象,所以无论我如何初始化sheeps,经过UpdateModel之后,sheeps对象就会被覆盖成页面最新绑定过来的对象!
        // 摘要:
        //     Updates the specified model instance using values from the controller's current
        //     value provider.
        //
        // 参数:
        //   model:
        //     The model instance to update.
        //
        // 类型参数:
        //   TModel:
        //     The type of the model object.
        //
        // 异常:
        //   System.InvalidOperationException:
        //     The model was not successfully updated.
        protected internal void UpdateModel<TModel>(TModel model) where TModel : class;

二.限制绑定到的特定数据源

之前介绍过,默认的模型绑定器,查找数据源的顺序是:


1.用户在HTML表单form中提供的值;(即Request.Form)
2.从应用程序路由获得的值;(即RouteData.Values)
3.URL中查询字符串部分的值;(即Request.QueryString)
4.请求中的上传文件。(即Request.Files)

那如果我只想让绑定器查找表单中的数据,怎么办呢?
很简单,只需要给上述方法加一个参数即可:
      public ActionResult ModelBindSheep()
        {
            Dictionary<string, Sheep> sheeps=null;
            if (sheeps == null)
            {
                sheeps = new Dictionary<string, Sheep>();
                sheeps.Add("firstSheep", new Sheep { Name="Tom" , AddInfo=new AdditionalInformation{Country="China",City="ShangHai"}});
                sheeps.Add("secondSheep", new Sheep { Name = "Jack", AddInfo = new AdditionalInformation { Country = "America", City = "sanfrancisco" }});
                sheeps.Add("thirdSheep", new Sheep { Name = "Jerry", AddInfo = new AdditionalInformation { Country = "France", City = "London" }});
            }

            UpdateModel(sheeps,new FormValueProvider(ControllerContext));//就是第二个参数

            return View(sheeps);
        }

或者:

      public ActionResult ModelBindSheep(FormValueProvider formData)
        {
            Dictionary<string, Sheep> sheeps = null;
            if (sheeps == null)
            {
                sheeps = new Dictionary<string, Sheep>();
                sheeps.Add("firstSheep", new Sheep { Name = "Tom", AddInfo = new AdditionalInformation { Country = "China", City = "ShangHai" } });
                sheeps.Add("secondSheep", new Sheep { Name = "Jack", AddInfo = new AdditionalInformation { Country = "America", City = "sanfrancisco" } });
                sheeps.Add("thirdSheep", new Sheep { Name = "Jerry", AddInfo = new AdditionalInformation { Country = "France", City = "London" } });
            }

            UpdateModel(sheeps, formData);//就是第二个参数

            return View(sheeps);
        }
上述是只想让其查找表单中的数据源,若要限制成只查找其它数据源,只需UpdateModel第二个参数即可,四个数据源位置对应的参数类型分别是:

IValueProvider实现
Request.Form FormValueProvider
RouteData.Values RouteDataValueProvider
Request.QueryString QueryStringValueProvider
Request.Files HttpFileCollectionValueProvider
注:如果使用UpdateModel方法,就要小心了。如果我们绑定的数据源的值并不能绑定到相应模型属性的值(如模型属性是个数字型,而绑定的数据源的值是字符串型,这时候模型绑定可能就会出异常,所以就要借助try...catch:
            try 
	        {	        
                    UpdateModel(sheeps,new FormValueProvider(ControllerContext));
	        }
	        catch (InvalidOperationException ex)
	        {
		        //...
	        }

或者借助TryUpdateModel:

            if (TryUpdateModel(sheeps,new FormValueProvider(ControllerContext)))
	        {
		        //...
	        }

三.自定义模型绑定,实现文件上传

想要自定义模型绑定,只要自己定义一个类,让其继承IModelBinder接口即可,下面随意定义一个类:
public class DefineModelBind : IModelBinder

继承该接口的话,就需要自己来完成接口中的BindModel方法啦,因为我要上传文件,那么提交表单后,我的action参数肯定是一个文件集合咯,所以BindModel方法就是返回一个文件集合:

        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }

            if (bindingContext == null)
            {
                throw new ArgumentNullException("bindingContext");
            }

            //获得用户已经上传的文件集合
            HttpFileCollectionBase uploadFiles = controllerContext.HttpContext.Request.Files;

            //获得需要绑定的模型名称(即action的参数名称,亦即file控件的name属性名称)
            string modelName = bindingContext.ModelName;

            //获得与模型名称匹配的文件,并将它们转化成列表
            List<HttpPostedFileBase> postedFiles = uploadFiles.AllKeys
                .Select((thisKey, index) => (String.Equals(thisKey, modelName, StringComparison.OrdinalIgnoreCase)) ? index : -1)
                .Where(index => index >= 0)
                .Select(index => uploadFiles[index])
                .ToList();

            //返回绑定好的对象(这里是一个文件集合)
            if (postedFiles.Count == 0)
            {
                return null;
            }
            else
            {
                return postedFiles;
            }

        }

光有上面的具体实现代码是不行的,因为MVC压根就不知道我定义的这个模型绑定,还要在Global.asax.cs文件中注册一下我自定义的绑定器:

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            // 默认情况下对 Entity Framework 使用 LocalDB
            Database.DefaultConnectionFactory = new SqlConnectionFactory(@"Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True");

            //利用我的自定义绑定器来绑定一个HttpPostedFileBase列表
            ModelBinders.Binders[typeof(List<HttpPostedFileBase>)] = new DefineModelBind();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
        }

OK,一切搞定,这时候就可以实验一下能否上传文件了,为了简单演示,我就以上传图片为例:

1)在网站根目录下新建一个文件夹Uploads用于保存上传的文件;
2)修改控制器中的Index方法:
  public ActionResult Index()
        {
            var model = Directory.GetFiles(Server.MapPath(@"\Uploads\")).AsEnumerable().ToList(); //获取Uploads目录下的图片列表

            for (int i = 0; i < model.Count(); i++)
            {
                string[] filesplit = model[i].Split('\\');  //把路径拆分成数组
                model[i] = filesplit[filesplit.Length - 1]; //最后一项就是文件名称

            }
            if (model!=null)
            {
                return View(model);
            }
            else
            {
                return View();
            }
        }
并添加另一个方法:
public ActionResult HandleUploadFiles([ModelBinder(typeof(DefineModelBind))] IList<HttpPostedFileBase> files)
        {
            if (files.Count() == 0)
            {
                return RedirectToAction("Index");
            }

            foreach (HttpPostedFileBase file in files)
            {
                if (file==null)
                {
                    return RedirectToAction("Index");
                }
                if (file.ContentLength != 0)
                {
                    string[] filesplit = file.FileName.Split('\\');         //把路径拆分成数组
                    string fileName = filesplit[filesplit.Length - 1]; //最后一项就是文件名称
                    file.SaveAs(Server.MapPath(@"\Uploads\") + fileName); //保存到服务器Uploads目录下           
                }
            }

            var model = Directory.GetFiles(Server.MapPath(@"\Uploads\")).AsEnumerable().ToList(); //获取Uploads目录下的图片列表

            for (int i = 0; i < model.Count(); i++)
            {
                string[] filesplit = model[i].Split('\\');  //把路径拆分成数组
                model[i] = filesplit[filesplit.Length - 1]; //最后一项就是文件名称
                
            }

            if (model!=null)
            {
                return RedirectToAction("Index",model);
            }
            else
            {
                return RedirectToAction("Index");
            }
            
        }

3)为Index方法新建一张默认视图,修改其代码为:

@model IEnumerable<string>

@{
    ViewBag.Title = "Index";
}

<h2>自定义模型绑定,绑定上传文件!</h2>

@using (Html.BeginForm("HandleUploadFiles", "Upload", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    <p><input type="file" name="files" /> </p>

    <input type="submit" value="开始上传" /> 
}

<script language="JavaScript" type="text/javascript">
    function AdjustImageSize(img) {
        img.width = img.width / 3;
    }
</script>

@if (Model!=null)
{
    <p>您已经上传的图片有:</p>
    <br />
    foreach (string fileName in Model.ToList())
    {
        <img src ="../../Uploads/@fileName"   alt ="image"   onload="AdjustImageSize(this)"  />   
    }
}

以上任何代码若出现需解析引用的提示,需自行添加using。

编译运行,上传几张图片试试:
OK,成功上传!
总结,关于上面的示例,需要澄清一下原理:
1)当我选择浏览好文件后,确定;
2)点击开始上传按钮,这时候表单被提交了。于是就执行了HandleUploadFiles方法,该方法有一个参数files,而且注解属性要求使用我的自定义绑定器;
3)自定义绑定器根据HandleUploadFiles的参数名(这里是files)找到页面中name属性的值也是files的数据源;这里是:
<input type="file" name="files" />

4)于是自定义绑定器接收该数据源的值,并转化成HandleUploadFiles参数需要的对象(这里是文件列表);

5)动作调用器接收上面传过来的文件列表参数,并执行HandleUploadFiles方法;
于是就成功利用自定义模型绑定上传了文件偷笑以上,HandleUploadFiles参数名和页面file控件的name属性名必须一致,否则模型绑定会失败。

抱歉!评论已关闭.