React & ASP.NET C# Web API with SQL Server – Multiple File Uploads to Folder

Photo by Leah Kelley from Pexels

Overview

This is an example of a React CRUD SPA that can upload multiple files with ASP.NET Web API. The files are uploaded

The API can be found here and will be included in the GitHub Repository soon.

A quick walk through of what happens in this small application.


Before you can run the API

Try running the following command in the Package Manager Console

use nuget locals all -clear 

OR

dotnet nuget locals all --clear

Also may need to update the installed Nuget Packages.

Then Cloning this repo will require you to install the nuget packages. Try an update to get everything installed

Prerequisites

Install the following libraries

npm i axios
npm i bootstrap
npm i @material-ui/cor
npm i @material-ui/icons
npm i material-ui-dropzone
npm i react-responsive-carousel

Create 3 components labeled FileUploadList, FileUploadLayout, and ImageContainer.

"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"material-ui-dropzone": "^3.5.0",
"axios": "^0.20.0",

Form Component

ImportsLibraries

Import the following libraries for the form component.

import React from 'react'
import axios, { post } from 'axios';
import AttachFile from '@material-ui/icons/AttachFile';
import {DropzoneArea} from 'material-ui-dropzone';
import 'bootstrap/dist/css/bootstrap.min.css';
import { Container, Form, Button, Alert, Card } from 'react-bootstrap';

Declare and initialize the variables.

class FileUploadForm extends React.Component {

  constructor(props) {
    super(props);
    this.state ={
      UserId: 1,
      Title: '',
      files: [],
      filename: '',
      message: ''
    }
    this.onFormSubmit = this.onFormSubmit.bind(this)
    this.onChange = this.onChange.bind(this)

  }

Dropzone

The functions below are events for the drop zone.


 handleChange(files) {

    this.setState({
      files: files
    });
 }
  
 onChangeInput(event) {
    console.log('event ', event);
    const target = event.target;        
    const name = target.name;
    var value = target.value;     
    this.setState({
        [name]: value
    });
  }

  onChangeFieldField(e) {
    this.setState({file:e.target.files[0], filename: e.target.files[0].name})
  }
  
  onChange(e) {
    this.setState({file:e.target.files[0]})    
  }

 

Handling Form Submit

This function will get the title and put the files into an array before posting the data using Axios.

The part that most developers including myself get stuck on, is generating the array of files to post. The solution is a loop appending the files to a dynamically created array. In the snippet below are files added to images[].

for (let i = 0; i < this.state.files.length; i++) {
    formData.append(`images[${i}]`, this.state.files[i])
}

The onFormSubmit Function in it entirety.

onFormSubmit(e){
    e.preventDefault() // Stop form submit
    const apiurl = `http://localhost:60408/api/product/add`;
    
    const formData = new FormData()
    // add a non-binary file    
    formData.append('Title', this.state.Title)    

    console.log('formData ', formData);
    var parent = this; // Cannot call props directly inside of Axios response
    var counter = 0;
    

    for (let i = 0; i < this.state.files.length; i++) {
        formData.append(`images[${i}]`, this.state.files[i])
    }

    axios({
      method: 'post',
      url: apiurl,
      data: formData,
      headers: {'Content-Type': 'multipart/form-data' }
      })
      .then(function (response) {
          //handle success
          // add a non-binary file    
          console.log(response);
      })
      .catch(function (response) {
          //handle error
          console.log(response);
      });

  }

Render the Form

 render() {
    
    return (
      <React.Fragment>
        <Form onSubmit={this.onFormSubmit}>
          <div className="row">
            <div className="col">
              <input type="hidden" name="UserID" value="1" />
                <Form.Group >
                  <Form.Label className="left">Title</Form.Label>
                  <Form.Control type="text" placeholder="File Title" name="Title" id="Title" onChange={this.onChangeInput.bind(this)}/>
                  <Form.Text className="text-muted">
                  Title for Searching
                  </Form.Text>
                </Form.Group>
            </div>
          </div>
          <div className="row mt-3">
            <div className="col">
             <DropzoneArea
                onChange={this.handleChange.bind(this)}
                />
             </div>
            </div>
            <div className="row mt-5">
              <div className="col text-left">
                <Button variant="primary" type="submit">
                    Submit Form
                </Button>
              </div>
            </div>
            
        </Form>

      </React.Fragment>
    )
  }
}

Layout & List of Records

The Layout and List of Records (Optional)

FileUploadLayout.js – Puts the Form on the left side and a list of records on the right side. When a record is added the form child component alerts the parent and the parent runs load in the child list component.

import React from 'react'
import { Container, Form, Button, Alert, Card } from 'react-bootstrap';
import './FileUploadForm.css';
import FileUploadList from './FileUploadList';
import FileUploadForm from './FileUploadForm';
import 'bootstrap/dist/css/bootstrap.min.css';

export default class FileUploadLayout extends React.Component {

  constructor(props) {
    super(props);    
    this.child = React.createRef();
  }

  onAddFile = (newFile) => {      
      this.child.current.loadData();
  }
  
  render() {
    
    const isAlert = false; 
    return (
      <React.Fragment>
        <Container>    
          <div className="row">
            <div className="col">
              <Card style={{ width: '28rem' }} className="center">            
              <Card.Body>    
                  <FileUploadForm onAddFile={this.onAddFile.bind(this, 'File Added')}/>
              </Card.Body>
              </Card>
            </div>
            <div className="col">
                <Card style={{ width: '35rem', height: '40rem' }} className="center">            
                <Card.Body>
                  <FileUploadList ref={this.child} />
                </Card.Body>
                </Card>
              </div>
            </div>
        </Container>
      </React.Fragment>
    )
  }
}

FileUploadList.js – List of Products and the Product Images.

Warning: This is not pretty. It’s just a working example.

Getting the data using axios.get() call from the componentDIdMount event.


componentDidMount() {
  this.loadData();
}

loadData() {
    const apiurl = `http://localhost:60408/api/product/get`;
    
    axios({
        method: 'get',
        url: apiurl,
          headers: {'Content-Type': 'multipart/form-data' }
        })
        .then((response) => {
            
            console.log(response);
            
            var dataArr = [];
            var imageArr = [];
            var tmpArr = response.data.data.record;
            console.log('tmpArr ', tmpArr);                
            tmpArr.forEach(element => {                                                    
              imageArr = [];
              element.Images.forEach(img => {
                console.log('img ', img);                
                const path = `http://localhost:60408/Content/images/${img.ImagePath}`;
                
                imageArr.push({original: path, thumbnail: path});
              });                  
              console.log('imageArr ', imageArr);                
              dataArr.push({Id: element.Id, Title: element.Title, Images: imageArr});
              console.log('dataArr ', dataArr);                
            })
            this.setState({data: dataArr});
            console.log(this.state.data);                
        })
        .catch((e) => 
        {
        console.error(e);
        });        
}
  

Deleting with axios.delete()

Call the API to remove the records in the ProductImages table in addition to deleting the files from the directory. After the images have been deleted, the parent record is removed. This is the order you want to see if there are constraints on the tables to prevent deleting from the parent table if there are records in the child table.

import React from 'react'
import axios, { post } from 'axios';
import ImageContainer from './ImageContainer';
import 'bootstrap/dist/css/bootstrap.min.css';
import './FileUploadForm.css';

export default class FileUploadList extends React.Component {

    constructor(props) {
      super(props);
      this.state = { 
        data: [], 
        queryStr: ''
      };        
    }

    componentDidMount() {
      this.loadData();
    }

    loadData() {
        const apiurl = `http://localhost:60408/api/product/get`;
        
        axios({
            method: 'get',
            url: apiurl,
              headers: {'Content-Type': 'multipart/form-data' }
            })
            .then((response) => {
                
                console.log(response);
                
                var dataArr = [];
                var imageArr = [];
                var tmpArr = response.data.data.record;
                console.log('tmpArr ', tmpArr);                
                tmpArr.forEach(element => {                                                    
                  imageArr = [];
                  element.Images.forEach(img => {
                    console.log('img ', img);                
                    const path = `http://localhost:60408/Content/images/${img.ImagePath}`;
                    
                    imageArr.push({original: path, thumbnail: path});
                  });                  
                  console.log('imageArr ', imageArr);                
                  dataArr.push({Id: element.Id, Title: element.Title, Images: imageArr});
                  console.log('dataArr ', dataArr);                
                })
                this.setState({data: dataArr});
                console.log(this.state.data);                
            })
            .catch((e) => 
            {
            console.error(e);
            });        
    }
     
 
    onDelete(val) {      
      const url = `http://localhost:60408/api/product/remove/${val}`;
      var self = this;
      axios.delete(url)       
      .then((response) => {            
        console.log(response);        
        self.loadData();
      })
      .catch((e) => 
      {
        console.error(e);
      });        
    }

    render() {    

      return (
        <React.Fragment>
          <table className="table">
          <thead>
            <tr>              
              <th scope="col">Title</th>
              <th scope="col"></th>
              <th scope="col"></th>
            </tr>
          </thead>
          <tbody style={{height: '10px !important', overflow: 'scroll'}}>
              {this.state.data.map((item, key) => {
                return (
                <tr key={item.Id}>                
                  <td><ImageContainer data={item.Images} /></td>           
                  <td width="400">{item.Title}</td>                                 
                  <td>
                    <Button variant="contained" color="secondary" onClick={() => this.onDelete(item.Id)}>
                      Delete
                    </Button>
                  </td>           
                </tr>
                )
            })}

          </tbody>
          </table>

        </React.Fragment>
        )
    }
}
Photo by Ann H from Pexels