views:

1790

answers:

4

Is there any way to get posted files (<input type="file" />) to take part in model binding in ASP.NET MVC without manually looking at the request context in a custom model binder, and without creating a separate action method which only takes a posted file as input?

I would have thought that this would work:

class MyModel {
  public HttpPostedFileBase MyFile { get; set; }
  public int? OtherProperty { get; set; }
}

<form enctype="multipart/form-data">
  <input type="file" name="MyFile" />
  <input type="text" name="OtherProperty" />
</form>

public ActionResult Create(MyModel myModel) { ... }

But given the above scenario, MyFile isn't even part of the value provider values in the binding context. (OtherProperty is, of course.) But it works if I do this:

public ActionResult Create(HttpPostedFileBase postedFile, ...) { ... }

So, why does no binding occur when the parameter is a model, and how can I make it work? I have no problem with using a custom model binder, but how can I do this in a custom model binder without looking at Request.Files["MyFile"]?

For consistency, clarity and testability, I'd like my code to provide automatic binding of all properties on a model, including those bound to posted files, without manually inspecting the request context. I am currently testing model binding using the approach Scott Hanselman wrote about.

Or am I going about this in the wrong way? How would you solve this? Or is this not possible by design due to the history of separation between Request.Form and Request.Files?

A: 

Have you looked at this post which he links to from the one you linked to (via another one...)?

If not, it looks quite simple. This is the model binder he uses:

public class HttpPostedFileBaseModelBinder : IModelBinder {
    public ModelBinderResult BindModel(ModelBindingContext bindingContext) {
        HttpPostedFileBase theFile =
            bindingContext.HttpContext.Request.Files[bindingContext.ModelName];
        return new ModelBinderResult(theFile);
    }
}

He registers it in Global.asax.cs as follows:

ModelBinders.Binders[typeof(HttpPostedFileBase)] = 
    new HttpPostedFileBaseModelBinder();

and posts with a form that looks like this:

<form action="/File/UploadAFile" enctype="multipart/form-data" method="post">
    Choose file: <input type="file" name="theFile" />
    <input type="submit" />
</form>

All the code is copied straight off the blog post...

Tomas Lycken
Actually, this is the approach I'm currently using. But there are two problems with this approach: 1; it uses the request context (via bindingContext.HttpContext.Request), which I really don't want to, and 2; it only handles a scenario where the post only consists of an uploaded file (this could of course easily be changed though).
bzlm
Also, BindModel(ModelBindingContext bindingContext) looks like pre-release code. There's a ControllerContext in there as well.
bzlm
Have you taken a look at the MVC Framework source code? I'm not sure how the "normal" model binders work, but I can't really see how you'd be able to grab form values without using the HttpContext.Current.Request.Form[] collection *somewhere* along the road... http://weblogs.asp.net/scottgu/archive/2008/03/21/asp-net-mvc-source-code-now-available.aspx
Tomas Lycken
Actually, I just took a look in the ASP.NET MVC 1.0 source, and the HttpPostedFileBaseModelBinder is in fact built into the framework. You should be able to bind several other parameters as well, by using a signature like ActionResult(HttpPostedFileBase theFile, string theFileName, string theDescription).
Tomas Lycken
Yeah. And my question is: Is there no way to do this using an action that has a *model* as its parameter, and not only a posted file? It seems a bit limited to have to restrict one action method to one parameter to make this work automatically.
bzlm
A: 

You don't need to register a custom binder, HttpPostedFileBase is registered by default in the framework:

public ActionResult Create(HttpPostedFileBase myFile)
{
    ...
}

It helps to read a book every once in awhile, instead of relying solely on blogs and web forums.

DSO
I've edited the question to be more clear about the fact that I do not want an action method that takes a posted file as its sole parameter. Is there any way to accomplish the same if the posted file is just one of many properties on the model? (I have the other ASP.NET MVC book, by the way. :)
bzlm
This is the correct answer; I had the same problem a while back. HttpPostedFileBase will bind but HttpPostedFile will not.
Nathan Ridley
No, HttpPostedFileBase will not bind using the standard value provider binding in the default model binder. It will bind in the method in this answer, though.
bzlm
Also helps if you read the question fully and carefully before being smarmy ;)
James Allen
+4  A: 

It turns out the reason is that ValueProviderDictionary only looks in Request.Form, RouteData and Request.QueryString to populate the value provider dictionary in the model binding context. So there's no way for a custom model binder to allow posted files to participate in model binding without inspecting the files collection in the request context directly. This is the closest way I've found to accomplish the same thing:

public ActionResult Create(MyModel myModel, HttpPostedFileBase myModelFile) { }

As long as myModelFile is actually the name of the file input form field, there's no need for any custom stuff.

bzlm
+1  A: 

Another way is to add a hidden field with the same name as the input:

<input type="hidden" name="MyFile" id="MyFileSubmitPlaceHolder" />

The DefaultModelBinder will then see a field and create the correct binder.

Brian Chance
That works. Thanks!
James Allen
Looks like ASP.NET MVC 2 RC handles the this without the hidden field.
Brian Chance