Web apps often need to upload files. ModelBinding a file upload to an HttpPostedFileBase action parameter is straightforward as long as you know the exact name of the file upload html control. There may be some situations where there are quite a few file upload controls and for whatever reason, they have very different names (they could be autogenerated on the page for example). In such a case, it becomes difficult to know what names to give to the action parameters at compile time. The usual option would be to hook into the Request parameters. That works, but is not very testable (without a good mocking framework like SharpMock, Moles, TypeMock Isolator etc.). In such situations, we can use a model binder to bind all file uploads to a single upload parameter.
The Container
The first thing to do is to create the class to hold the information relating to the uploaded files:
public class UploadedFileInfo { public string Name { get; private set; } public HttpPostedFileBase File { get; private set; } public UploadedFileInfo(string name, HttpPostedFileBase file) { Name = name; File = file; } }
The class is pretty simple. It holds the name of the file upload control that uploaded a file and the uploaded file itself.
The ModelBinder
Next comes the ModelBinder:
public class UploadedFileInfoArrayBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var files = controllerContext.HttpContext.Request.Files; var list = new List<UploadedFileInfo>(); for (int i = 0; i < files.Count; i++) { var file = files[i]; var name = files.AllKeys[i]; var fileInfo = new UploadedFileInfo(name, file); list.Add(fileInfo); } return list.ToArray(); } }
Basically, the binder looks into Request.Files and for each file present, it creates and instance of UploadedFileInfo and adds it to a list. It simply returns the list as an array. Please note that if a user does not select a file for an upload control, it will still have an entry in Request.Files, only the uploaded file's ContentLength would be zero.
The Controller
And the controller:
public class UploadTestController : Controller { [HttpGet] public ActionResult Index() { return View(); } [HttpPost] public ActionResult Index(UploadedFileInfo[] files) { var uploadedCount = files.Where(x => x.File.ContentLength > 0).Count(); ViewData["Message"] = string.Format("{0} file(s) uploaded successfully", uploadedCount); return View(); } }
The GET Index() is trivial. The Post Index() fetches the number of files in the model-bound files array that have a non-zero content length and passes the info to the View.
The View
And the view:
<body> <div> <form enctype="multipart/form-data" method="post"> <input type='file' name='file1' id='file1' /> <input type='file' name='file2' id='file2' /> <input type="submit" value='upload' /> </form> </div> <br /> <%: ViewData["Message"] %> </body>
The View is the same for the GET and the POST (don't mind the fact that I should be redirecting after the post – this is a demo after all!). There's a form that has two file uploads in a form. The file uploads have different names. The View also spits ViewData["Message"] if present.
Assigning the ModelBinder in Global.asax.cs
At this point, our code still won't work as the ModelBinder is not being applied to the files parameter of the POST Index(). We can add an attribute to the parameter, but there's a cleaner, nicer way to set it up in the global.asax.cs file:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); ModelBinders.Binders[typeof(UploadedFileInfo[])] = new UploadedFileInfoArrayBinder(); }
In the last line, we're basically telling MVC that whenever it encounters a parameter to an ActionMethod that is an array of UploadedFileInfo, it should use an UploadedFileInfoArrayBinder. The binder will then fetch the files from the Request and assign it to the parameter.
Running the App
With all this in place, we can run the app, select a couple of files and hitting upload should tell us that the files have been uploaded.
Source
---------------------------------------------------------
Questions and comments relating to this article are welcome. Comments completely unrelated to the article and posted with the sole intention of putting your link here are not.
If you spam, your comment will not be approved, will be deleted and your IP blocked. I maintain my site almost daily and such comments – even if they pass the spam filter – will get removed as soon as possible. If this gets too tedious, I may disable comments entirely. Please don't ruin it for everybody else.
---------------------------------------------------------