2

I have been stuck on this for a couple hours now and not even google can help anymore. I am trying to send a file from the client to the backend using xmlhttprequest. I cannot get the filename, type, or content on the C# side. I would appreciate help on doing this. A lot of code I came across had methods that I can only guess are not supported in ASP.Net 5 and MVC 6 (such as HttpContext.Current and HttpPostedFile)

Here is my client side JavaScript request. This sends the query strings which bind to my model no problem so that is easily accessible, but getting the file is what I am having trouble with.

var form = new FormData();
form.append("file", file);

var queryParams = "id=" + (id == null ? -1 : id);
queryParams += "&name=" + name;
queryParams += "&value=" + val;

xhrAttach(REST_DATA + "/attach?" + queryParams, form, function (item) {

    console.log('attached: ', item);
    alert(item.responseText);
    row.setAttribute('data-id', item.id);
    removeProgressIndicator(row);
    setRowContent(item, row);
}, function (err) {
    console.log(err);
    //stop showing loading message
    stopLoadingMessage();
    document.getElementById('errorDiv').innerHTML = err;
});

function xhrAttach(url, data, callback, errback)
{
    var xhr = new createXHR();
    xhr.open("POST", url, true);
    //xhr.setRequestHeader("Content-type", "multipart/form-data");
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
            if(xhr.status == 200){
                callback(parseJson(xhr.responseText));
            }else{
                errback("Error: "+xhr.responseText);
            }
        }
    };
    xhr.timeout = 1000000;
    xhr.ontimeout = errback;
    xhr.send(data);
}

Here is my Controller dealing with the request. attachment is a model and the query string binds to it no problem. I could not find out how to add a File parameter to the model, or if that would even matter. Things I have tried are under this code.

    // POST: /api/db/attach
    [Route("/api/[controller]/attach")]
    [HttpPost]
    public async Task<dynamic> attach(Attachment attachment)
    {
        //get the file somehow

    }

i have tried many things, but cannot remember exactly what, here is one thing I did try though, which did not work.

var file = Request.Form["file"];

here is the attachment model in case it helps

namespace MyModel.Models
{
    public class Attachment
    {
        public long id { get; set; }
        public string name { get; set; }
        public string value { get; set; }
    }
}
Andrew Lohr
  • 5,380
  • 1
  • 26
  • 38
  • Could you share how the raw request looks like? (example: from Fiddler or browser tools may be) – Kiran Mar 30 '15 at 21:41

3 Answers3

1

Don't use query parameters or FormData if you're going to use a model on the MVC side. Just don't. And to me, it's better to just get the file into a base64 string first, than to try sending the File object, itself. I've posted about how to do that, here: Convert input=file to byte array

Then, declare and format a JSON object:

var dataObj = {
    file = fileByteArray[0],
    id = (id == null ? -1 : id),
    name = name,
    value = val
};

That fileByteArray[0] is referencing the object from my link. My answer there assumes you were just going to keep loading file base64 strings into that global array object. You can either keep it as an array, like I had, and loop through them one by one, replacing that [0] with [i], for example, as the indexer in a for loop, or just use a var fileByteArray = "" with that other code, make it so you don't push additional files but always just overwrite that variable, & just use that.

And a word of caution on that last parameter - don't use val if you use jQuery - it's a keyword. I only have it above because it's what you were passing to the URL parameter values.

Get rid of the queryParams in this line:

xhrAttach(REST_DATA + "/attach?" + queryParams, form, function (item) {

Change it to:

xhrAttach(REST_DATA + "/attach", form, function (item) {

Set the Content-Type, right where it's commented out, to:

xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");

Change what you are sending - it's no longer going to be FormData, it's the JSON object, and it needs to be stringified:

xhr.send(JSON.stringify(dataObj));

Fix your model to now include the file base64 string:

public class Attachment
{
    public string file { get; set; }
    public long id { get; set; }
    public string name { get; set; }
    public string value { get; set; }
}

Fix your POST method. 2 issues:

  1. You can't use [HttpPost] if your class is inheriting ApiController, which you probably should be for this. It must be [System.Web.Http.HttpPost], and yes, it has to be completely spelled out, or it will assume it's [System.Web.Mvc.HttpPost] and not assign the route - you'd get a 404 - Not Found error when you try to do your POST. If you're inheriting from Controller, disregard this.
  2. You will need a [FromBody] tag on your model if you are inheriting from ApiController:

    public async Task<dynamic> attach([FromBody]Attachment attachment) { ... }

Then you get the file like this:

string base64FileString = attachment.file;

If you want to store it in a byte[] in the database, you can convert it:

byte[] bytes = System.Convert.FromBase64String(base64FileString);

And btw, I think your response handling is wrong. I would not do this:

xhr.onreadystatechange = function(){
    if(xhr.readyState == 4){
        if(xhr.status == 200){
            callback(parseJson(xhr.responseText));
        }else{
            errback("Error: "+xhr.responseText);
        }
    }
};

This is how I would do it:

xhr.onreadystatechange = function(response){
    if(xhr.readyState == 4 && xhr.status == 200){
            callback(parseJson(response.target.responseText));
    } else {
            alert("Error: " + response.target.responseText);
    }
};

Assuming that the response.target.responseText is getting the error sent back from the server-side in a way you can display. If not, sending it to a function that could parse it out would be the right choice. I don't think that xhr.responseText was correct.

vapcguy
  • 7,097
  • 1
  • 56
  • 52
0

I would suggest trying the following:

public async Task<dynamic> attach([FromURI]Attachment attachment, [FromBody] FormDataCollection formData)

And then the FormDataCollection should have the form data for retrieval.

to11mtm
  • 169
  • 6
  • thanks for the response, it seems that [FormURI] and FormValueCollection got the error "the type or namespace could not be found" and there was no option to import anything. It this available in ASP.NET5 ? EDIT: looks like i was able to substitute [FromURI] with [FromQuery] and the attachment still works. But still nothing with formData. I tried switching FormDataCollection to dynamic as well and its null – Andrew Lohr Mar 29 '15 at 19:11
  • also switched [FromBody] to [FromForm] and switched FormDataCollection to dynamic and actually got an object. But i have no clue what the properties are and how to access its content, any help with this would be great. – Andrew Lohr Mar 29 '15 at 19:24
  • alright installed Microsoft.AspNet.WebApi.Client and that allowed me to use FormDataCollection. But still unable to access anything in formData. any method I used to print out some info results in an null error ref: NullReferenceException: Object reference not set to an instance of an object. – Andrew Lohr Mar 29 '15 at 19:44
  • Are you sure that the client is sending the form over in the body? I'd suggest looking at the network traffic using your browser's debugging tools to ensure the form data is being sent over. – to11mtm Mar 29 '15 at 21:00
  • yeah I am pretty sure, I see [this post data](http://imgur.com/gHg5M5a) is being sent to the server – Andrew Lohr Mar 29 '15 at 21:28
  • We may have been going about this a little wrong then... This example shows how to get multipart data in that format: http://www.asp.net/web-api/overview/advanced/sending-html-form-data,-part-2 – to11mtm Mar 29 '15 at 21:32
0

Add a public get/set property called File to your Attachment model and the uploaded file should be bound to this property.

A model similar to yours: https://github.com/aspnet/Mvc/blob/9f9dcbe6ec2e34d8a0dfae283cb5e40d8b94fdb7/test/WebSites/ModelBindingWebSite/Models/Book.cs#L8

public class Book
{
   public string Name { get; set; }

   public IFormFile File { get; set; }
}

Following controller has examples of different ways of model binding an uploaded file. https://github.com/aspnet/Mvc/blob/9f9dcbe6ec2e34d8a0dfae283cb5e40d8b94fdb7/test/WebSites/ModelBindingWebSite/Controllers/FileUploadController.cs#L81

public KeyValuePair<string, FileDetails> UploadModelWithFile(Book book)
        {
           var file = book.File;
            var reader = new StreamReader(file.OpenReadStream());
            var fileContent = reader.ReadToEnd();
            var parsedContentDisposition = ContentDispositionHeaderValue.Parse(file.ContentDisposition);
            var fileDetails = new FileDetails
            {
                Filename = parsedContentDisposition.FileName,
                Content = fileContent
            };

            return new KeyValuePair<string, FileDetails>(book.Name, fileDetails);
        }

if this does not work, then I suspect your request is not in correct format.

Kiran
  • 56,921
  • 15
  • 176
  • 161
  • Note: they use `IFormFile`in their `Book` model and I couldn't find a definition in their project for that, and putting `var fileContent` obfuscates the real type (and exactly why putting `var`, to me, is a BAD practice), but I believe it should be `byte[]` in the model for `Content`. – vapcguy Mar 23 '18 at 19:01
  • Also Note: posting like this required me to use JSON: `xhr.setRequestHeader("Content-type", "application/json;charset=UTF-8");`, and then you needed `[FromBody]Book book` up there and needed to tell him to ditch using the URL parameters and how to construct the JSON object. This answer leaves a lot to be desired and wasted a lot of time rummaging over C# code, when it's the JavaScript side of his that was the problem. – vapcguy Apr 06 '18 at 02:14