Dodawanie plików z React JS i .NET Core Web API
Cześć!
W ostatnim czasie miałem do czynienie z przesyłaniem pliku z poziomu aplikacji napisanej w React do Rest API postawionego w .NET core. Zainteresowanych takim rozwiązaniem zapraszam do przeczytania tego krótkiego wpisu :).
Dodawanie samego pliku
W tym punkcie pokażę, jak przesłać pojedynczy plik na serwer.
Na początek przygotuj kontroler po stronie serwera.
Route("uploader/justfile")] public dynamic UploadJustFile(IFormCollection form) { try { foreach (var file in form.Files) { UploadFile(file); } return new { Success = true }; } catch (Exception ex) { return new { Success = false, ex.Message }; } } private static void UploadFile(IFormFile file) { if (file == null || file.Length == 0) throw new Exception("File is empty!"); byte[] fileArray; using (var stream = file.OpenReadStream()) using (var memoryStream = new MemoryStream()) { stream.CopyTo(memoryStream); fileArray = memoryStream.ToArray(); } //TODO: You can do it what you want with you file, I just skip this step }
Jak widzisz utworzyłem prostą akcję kontrolera, która jako argument przyjmuje obiekt typu IFormCollection. Następnie przechodzi po plikach tej kolekcji i wywołuje metodę UploadFile, którą możesz zaimplementować w dowolny sposób.
Przejdźmy teraz do frontendu i utwórzmy prostą formę.
render() { return ( <div> <form> <h2>Just file</h2> <p><b>{this.state.justFileServiceResponse}</b></p> <input type="file" id="case-one" onChange={this.filesOnChange} /> <br /> <button type="text" onClick={this.uploadJustFile}>Upload just file</button> </form> </div> ); }
Zawiera ona tylko jeden input oraz button wywołujący metodę uploadu. W metodzie filesOnChange, która jest wywoływana podczas eventu onChange, przypisywany jest do state nasz plik.
filesOnChange(sender) { let files = sender.target.files; let state = this.state; this.setState({ ...state, files: files }); }
Metoda uploadJustFile prezentuje się następująco
uploadJustFile(e) { e.preventDefault(); let state = this.state; this.setState({ ...state, justFileServiceResponse: 'Please wait' }); if (!state.hasOwnProperty('files')) { this.setState({ ...state, justFileServiceResponse: 'First select a file!' }); return; } let form = new FormData(); for (var index = 0; index < state.files.length; index++) { var element = state.files[index]; form.append('file', element); } axios.post('uploader/justfile', form) .then((result) => { let message = "Success!" if (!result.data.success) { message = result.data.message; } this.setState({ ...state, justFileServiceResponse: message }); }) .catch((ex) => { console.error(ex); }); }
Na początek zostaje zwalidowany state pod kątem sprawdzenia czy znajdują się jakieś pliki. Jeżeli tak, tworzę obiekt FormData do którego dodałem pliki z pola input. Na koniec wywołałem Web API.
Dodawanie pliku oraz zawartości formy
W kontrolerze dodaj kolejne dwie metody UploadFile oraz MapFormCollectionToPerson. Dodaj także klasę Person, która będzie modelem dla przychodzących danych.
Person.cs
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public string PhoneNumber { get; set; } }
UploaderController.cs
[Route("uploader/upload")] public dynamic Upload(IFormCollection form) { try { Person person = MapFormCollectionToPerson(form); foreach (var file in form.Files) { UploadFile(file); } return new { Success = true }; } catch (Exception ex) { return new { Success = false, ex.Message }; } } private static Person MapFormCollectionToPerson(IFormCollection form) { var person = new Person(); string firstNameKey = "firstName"; string lastNameKey = "lastName"; string phoneNumberKey = "phoneNumber"; if (form.Any()) { if (form.Keys.Contains(firstNameKey)) person.FirstName = form[firstNameKey]; if (form.Keys.Contains(lastNameKey)) person.LastName = form[lastNameKey]; if (form.Keys.Contains(phoneNumberKey)) person.PhoneNumber = form[phoneNumberKey]; } return p }
Tak jak widzisz za dużo się nie zmieniło. Oprócz naszego pliku w obiekcie form zostanie również przekazana kolekcja pól naszej formy, a następnie zostaną one zmapowane.
Po stronie frontendu dodaj kolejną formę z kilkoma polami tekstowymi i polem typu file. Do pól tekstowych należy przypisać event, aktualizujący obiekt state oraz nową metodę przekazującą dane do web API.
uploadForm(e) { e.preventDefault(); let state = this.state; this.setState({ ...state, formServiceResponse: 'Please wait' }); if (!state.hasOwnProperty('files')) { this.setState({ ...state, formServiceResponse: 'First select a file!' }); return; } let form = new FormData(); for (var index = 0; index < state.files.length; index++) { var element = state.files[index]; form.append('file', element); } for (var key in state.fields) { if (state.fields.hasOwnProperty(key)) { var element = state.fields[key]; form.append(key, element); } } axios.post('uploader/upload', form) .then((result) => { let message = "Success!" if (!result.data.success) { message = result.data.message; } this.setState({ ...state, formServiceResponse: message }); }) .catch((ex) => { console.error(ex); }); } filesOnChange(sender) { let files = sender.target.files; let state = this.state; this.setState({ ...state, files: files }); } fieldOnChange(sender) { let fieldName = sender.target.name; let value = sender.target.value; let state = this.state; this.setState({ ...state, fields: {...state.fields, [fieldName]: value} }); } render() { return ( <div> <form> <h2>Just file</h2> <p><b>{this.state.justFileServiceResponse}</b></p> <input type="file" id="case-one" onChange={this.filesOnChange} /> <br /> <button type="text" onClick={this.uploadJustFile}>Upload just file</button> </form> <hr /> <form> <h2>Form</h2> <p><b>{this.state.formServiceResponse}</b></p> <div> <input name="firstName" type="text" placeholder="First name" onChange={this.fieldOnChange} /> </div> <div> <input name="lastName" type="text" placeholder="Last name" onChange={this.fieldOnChange} /> </div> <div> <input name="phoneNumber" type="text" placeholder="Phone number" onChange={this.fieldOnChange} /> </div> <input type="file" onChange={this.filesOnChange} /> <br /> <button type="text" onClick={this.uploadForm}>Upload form </button> </form> </div> ); }
Metoda uploadForm wygląda podobnie do poprzedniej. Jedyna zmiana to dodanie wartości pól z obiektu state do naszego FormData i przesłanie obiektu na serwer.
Podsumowanie
Powyżej pokazałem jak w prostu sposób przekazać pliki z aplikacji klienckiej utworzonej w React na serwer, gdzie znajduje się Web API. Życzę miłej zabawy i z góry dziękuję za feedback.
GIT repository: https://github.com/artzie92/dotnetowo-upload-file-react-dotnet-core
Cześć,
Bardzo istotna uwaga:
We wszystkich przykładach ze state mutujesz go, tak nie można, setState wymaga nowego obiektu.
Tutaj wytłumaczenie:
https://facebook.github.io/react/docs/state-and-lifecycle.html#do-not-modify-state-directly
jeśli używasz ES6, możesz skorzystać z operatora spread (…):
np, zamiast:
let state = this.state;
state.fields[fieldName] = value;
this.setState(state);
użyłbyś
this.setState({
…state,
fields: { …state.fields, fieldName: value }
});
Pozdrawiam 🙂
Cześć,
Świetna uwaga, wielkie dzięki. Juz poprawiam! 🙂