0

I have a javascript form with a bunch of data that is appended to a form as follows:

                    this.historicalPreprintDocs.forEach((round, index) => {
                        form.append(`data[${index}].ppid`, this.selectPPID.PPID)
                        form.append(`data[${index}].round`, round.round)
                        form.append(`data[${index}].preprint`, round.preprint)
                        round.supportingDocumentation ? round.supportingDocumentation.forEach((file, index2) => {
                            form.append(`data[${index}].supportingDocumentation[${index2}]`, file)
                        }) : form.append(`data[${index}].supportingDocumentation`, null)
                    })

The issue is my model keeps binding as null for my controller. As soon as I use List for supportingDocumentation it breaks:

    public class HistoricalPreprintReview
{
    [ModelBinder(Name = "ppid")]
    public int ppid { get; set; }
    [ModelBinder(Name = "round")]
    public int round { get; set; }
    [ModelBinder(Name = "preprint")]
    public IFormFile preprint { get; set; }
    [ModelBinder(Name = "supportingDocumentation")]
    public List<IFormFile> supportingDocumentation { get; set; }
}

And my controller is as follows:

        [HttpPost]
    public async Task<ActionResult> UploadHistoricalReview([FromForm] List<HistoricalPreprintReview> data)
    {
// data comes back as null when stepping through and getting to supportingDocumentation
        }

I can see my payload it has the bindary data for each data[0].supportingDocumentation[0] , etc. so I am not sure why this does not work.

1 Answer 1

0

I am using new FormData() and formData.append(data[${index}].supportingDocumentation, supportingDocs[i]); to implement this feature, it works fine.

Here is my sample code

Index.cshtml

@{
    ViewData["Title"] = "Home Page";
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Historical Preprint Review Upload</title>
</head>
<body>
    <h1>Upload Historical Preprint Review</h1>
    <form id="uploadForm" enctype="multipart/form-data">
        <div id="roundsContainer">
            <div class="round">
                <h3>Round 1</h3>
                <label>PPID: <input type="text" name="ppid"></label><br>
                <label>Round: <input type="number" name="round"></label><br>
                <label>Preprint: <input type="file" name="preprint"></label><br>
                <label>Supporting Documentation: <input type="file" name="supportingDocumentation" multiple></label><br>
            </div>
        </div>
        <button type="button" onclick="addRound()">Add Another Round</button><br><br>
        <button type="button" onclick="submitForm()">Submit</button>
    </form>

    <script>
        let roundIndex = 1;

        function addRound() {
            roundIndex++;
            const roundsContainer = document.getElementById('roundsContainer');
            const newRound = document.createElement('div');
            newRound.classList.add('round');
            newRound.innerHTML = `
                <h3>Round ${roundIndex}</h3>
                <label>PPID: <input type="text" name="ppid"></label><br>
                <label>Round: <input type="number" name="round"></label><br>
                <label>Preprint: <input type="file" name="preprint"></label><br>
                <label>Supporting Documentation: <input type="file" name="supportingDocumentation" multiple></label><br>
            `;
            roundsContainer.appendChild(newRound);
        }

        async function submitForm() {
            const form = document.getElementById('uploadForm');
            const formData = new FormData();

            const rounds = form.querySelectorAll('.round');
            rounds.forEach((round, index) => {
                formData.append(`data[${index}].ppid`, round.querySelector('input[name="ppid"]').value);
                formData.append(`data[${index}].round`, round.querySelector('input[name="round"]').value);
                formData.append(`data[${index}].preprint`, round.querySelector('input[name="preprint"]').files[0]);

                const supportingDocs = round.querySelector('input[name="supportingDocumentation"]').files;
                for (let i = 0; i < supportingDocs.length; i++) {
                    formData.append(`data[${index}].supportingDocumentation`, supportingDocs[i]);
                }
            });

            try {
                const response = await fetch('/Home/UploadHistoricalReview', {
                    method: 'POST',
                    body: formData
                });
                const result = await response.json();
                console.log(result);
                alert('Upload successful!');
            } catch (error) {
                console.error('Error:', error);
                alert('Upload failed!');
            }
        }
    </script>
</body>
</html>

UploadHistoricalReview method

[HttpPost]
public async Task<ActionResult> UploadHistoricalReview([FromForm] List<HistoricalPreprintReview> data)
{
    return Ok(new { message = "Upload successful!" });
}

HistoricalPreprintReview.cs

public class HistoricalPreprintReview
{
    [ModelBinder(Name = "ppid")]
    public int ppid { get; set; }
    [ModelBinder(Name = "round")]
    public int round { get; set; }
    [ModelBinder(Name = "preprint")]
    public IFormFile preprint { get; set; }
    [ModelBinder(Name = "supportingDocumentation")]
    public List<IFormFile> supportingDocumentation { get; set; }
}

Test Result

enter image description here

enter image description here


Of course, we can also implement this by customizing ModelBinder.

HistoricalPreprintReviewModelBinder.cs

using Microsoft.AspNetCore.Mvc.ModelBinding;
using MVCDemo.Models;

namespace MVCDemo
{
    public class HistoricalPreprintReviewModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var form = bindingContext.HttpContext.Request.Form;
            var result = new List<HistoricalPreprintReview>();

            int index = 0;
            while (form.ContainsKey($"data[{index}].ppid"))
            {
                var review = new HistoricalPreprintReview
                {
                    ppid = int.Parse(form[$"data[{index}].ppid"]),
                    round = int.Parse(form[$"data[{index}].round"]),
                    preprint = form.Files.GetFile($"data[{index}].preprint")
                };

                var supportingDocumentation = form.Files
                    .Where(f => f.Name.StartsWith($"data[{index}].supportingDocumentation"))
                    .ToList();

                review.supportingDocumentation = supportingDocumentation;

                result.Add(review);
                index++;
            }

            bindingContext.Result = ModelBindingResult.Success(result);
            return Task.CompletedTask;
        }
    }
}

HistoricalPreprintReview.cs

[ModelBinder(BinderType = typeof(HistoricalPreprintReviewModelBinder))]
public class HistoricalPreprintReview
{
    public int ppid { get; set; }
    public int round { get; set; }
    public IFormFile preprint { get; set; }
    public List<IFormFile> supportingDocumentation { get; set; }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Your code works for me. My issue I am noticing is if the file is a zip file then the binding does not occur. Does IFormFile not work well with zip files?
Your code first set of code is the exact method I used originally and it was working intermittantly which is what led me to changing stuff the ultimately this question. I did not realize it was the zip file causing issues. Now I am scratching my head because I figured a zip file would be like any other file. In the payload it shows it as binary data.
And to make it even more confusing. It is only an issue if the zip file is trying to bind inside the List<IformFile>. It seems if I were to set preprint to zip it binds perfectly. If supportingDocumentation is a list of pdf or any other file type it works. But when there is a zip file in that list no binding occurs.
I am thinking this might be on the browser side. I also decided to test converting the files to base64 then upload them that approch and still zip files in the multiple selection option cause null binding even without iformfile.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.