پیشنیازها
«
بررسی روش آپلود فایلها در ASP.NET Core»
«
ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با
FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
تدارک مقدمات مثال این قسمت
این مثال در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه میکنیم:
>ng g m UploadFile -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و UploadFileModule را بجای UploadFileRoutingModule در قسمت imports معرفی میکنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت یک درخواست پشتیبانی را اضافه خواهیم کرد:
>ng g c UploadFile/UploadFileSimple
که اینکار سبب به روز رسانی فایل upload-file.module.ts و افزوده شدن UploadFileSimpleComponent به قسمت declarations آن میشود.
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف میکنیم:
>ng g cl UploadFile/Ticket
با این محتوا:
export class Ticket {
constructor(public description: string = "") {}
}
در اینجا Ticket تعریف شده دارای یک خاصیت توضیحات است و این فرم به همراه فیلد ارسال چندین فایل نیز میباشد که نیازی به درج آنها در کلاس فوق نیست:
ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن
پس از ایجاد ساختار کلاس Ticket، یک وهله از آنرا به نام model ایجاد کرده و در اختیار قالب آن قرار میدهیم:
import { Ticket } from "./../ticket";
export class UploadFileSimpleComponent implements OnInit {
model = new Ticket();
سپس قالب این کامپوننت و یا همان فایل upload-file-simple.component.html را به صورت ذیل تکمیل میکنیم:
<div class="container">
<h3>Support Form</h3>
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
<div class="form-group" [class.has-error]="description.invalid && description.touched">
<label class="control-label">Description</label>
<input #description="ngModel" required type="text" class="form-control"
name="description" [(ngModel)]="model.description">
<div *ngIf="description.invalid && description.touched">
<div class="alert alert-danger" *ngIf="description.errors.required">
description is required.
</div>
</div>
</div>
<div class="form-group">
<label class="control-label">Screenshot(s)</label>
<input #screenshotInput required type="file" multiple (change)="fileChange($event)"
class="form-control" name="screenshot">
</div>
<button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button>
</form>
</div>
در اینجا ابتدا فیلد توضیحات درخواست جدید، ارائه و به خاصیت model.description متصل شدهاست. همچنین این فیلد با ویژگی required مزین، و اجباری بودن آن بررسی گردیدهاست.
سپس در انتها، فیلد آپلود را مشاهده میکنید؛ با این ویژگیها:
الف) ngModel ایی به آن متصل نشدهاست؛ چون روش کار با آن متفاوت است.
ب) یک template reference variable به نام screenshotInput# در آن تعریف شدهاست. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
ج) به رخداد change این کنترل، متد fileChange متصل شدهاست که رخداد جاری را نیز دریافت میکند.
د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده میکنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.
دسترسی به المان ارسال فایل در کامپوننت متناظر
تا اینجا یک المان ارسال فایل را به فرم، اضافه کردهایم. اما چگونه باید به فایلهای آن برای ارسال به سرور دسترسی پیدا کنیم؟
برای این منظور در ادامه دو روش را بررسی خواهیم کرد:
1) دسترسی به المان ارسال فایل از طریق رخداد change
در تعریف فیلد ارسال فایل، اتصال به رخداد change تعریف شدهاست:
(change)="fileChange($event)"
معادل آن در سمت کامپوننت متناظر، به صورت ذیل است:
fileChange(event) {
const filesList: FileList = event.target.files;
console.log("fileChange() -> filesList", filesList);
}
همانطور که مشاهده میکنید، event.target، امکان دسترسی مستقیم به المان متناظری را در قالب کامپوننت میسر میکند. سپس میتوان به خاصیت files آن دسترسی یافت.
در اینجا ساختار شیء استاندارد FileList و اجزای آنرا مشاهده میکنید. برای مثال چون دو فایل انتخاب شدهاست، این لیست به همراه یک خاصیت طول و دو شیء File است.
تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آنها را شناسایی و intellisense متناظری را مهیا میکند:
C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
همچنین اگر به فایل tsconfig.json پروژه نیز مراجعه کنید، یک چنین تعاریفی در آن قرار دارند:
{
"lib": [
"es2016",
"dom"
]
}
}
وجود و تعریف کتابخانهی dom است که سبب کامپایل شدن کدهای فوق، بدون بروز هیچگونه خطایی میشود.
2) دسترسی به المان آپلود فایل از طریق یک template reference variable
در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شدهاست. میتوان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
import { Component, OnInit, ViewChild, ElementRef } from "@angular/core";
export class UploadFileSimpleComponent implements OnInit {
@ViewChild("screenshotInput") screenshotInput: ElementRef;
submitForm(form: NgForm) {
const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
console.log("fileInput.files", fileInput.files);
}
ابتدا یک خاصیت جدید را به نام screenshotInput از نوع ElementRef که در angular/core@ تعریف شدهاست، اضافه میکنیم. سپس برای اتصال آن به template reference variable ایی به نام screenshotInput، از ویژگی به نام ViewChild، با پارامتری مساوی نام همین متغیر، استفاده خواهیم کرد.
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شدهاست. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه میکنید، میتوان به خاصیت files این کنترل ارسال فایلها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شدهاست.
ارسال فرم درخواست پشتیبانی به سرور
تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون میخواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
>ng g s UploadFile/UploadFileSimple -m upload-file.module
که سبب به روز رسانی خودکار قسمت providers فایل upload-file.module.ts نیز میشود.
در ادامه کدهای کامل این سرویس را مشاهده میکنید:
import { Http, RequestOptions, Response, Headers } from "@angular/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/map";
import "rxjs/add/observable/of";
import { Ticket } from "./ticket";
@Injectable()
export class UploadFileSimpleService {
private baseUrl = "api/SimpleUpload";
constructor(private http: Http) {}
private extractData(res: Response) {
const body = res.json();
return body || {};
}
private handleError(error: Response): Observable<any> {
console.error("observable error: ", error);
return Observable.throw(error.statusText);
}
postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
if (!filesList || filesList.length === 0) {
return Observable.throw("Please select a file.");
}
const formData: FormData = new FormData();
for (const key in ticket) {
if (ticket.hasOwnProperty(key)) {
formData.append(key, ticket[key]);
}
}
for (let i = 0; i < filesList.length; i++) {
formData.append(filesList[i].name, filesList[i]);
}
const headers = new Headers();
headers.append("Accept", "application/json");
const options = new RequestOptions({ headers: headers });
return this.http
.post(`${this.baseUrl}/SaveTicket`, formData, options)
.map(this.extractData)
.catch(this.handleError);
}
}
توضیحات تکمیلی:
روش کار با فرمهایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرمهای معمولی. در فرمهای معمولی، اصل شیء Ticket را به متد this.http.post واگذار میکنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
الف) افزودن مقادیر خواص شیء Ticket به FormData
postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
const formData: FormData = new FormData();
for (const key in ticket) {
if (ticket.hasOwnProperty(key)) {
formData.append(key, ticket[key]);
}
}
با استفاده از حلقهی for میتوان بر روی خواص یک شیء جاوا اسکریپتی حرکت کرد. به این ترتیب میتوان نام و مقدار آنها را یافت و سپس به formData به صورت key/value افزود.
ب) افزودن فایلها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایلهای فرم است:
for (let i = 0; i < filesList.length; i++) {
formData.append(filesList[i].name, filesList[i]);
}
این مورد نیز به سادگی تشکیل یک حلقه، بر روی خاصیت files المان آپلود فایل است. به همین جهت بود که به دو روش سعی کردیم، به این خاصیت دسترسی پیدا کنیم.
یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمیتوان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار میتواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین میگردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده میکنیم.
ج) ارسال اطلاعات نهایی به سرور
اکنون که formData را بر اساس اطلاعات اضافی ticket و فایلهای متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
const headers = new Headers();
headers.append("Accept", "application/json");
const options = new RequestOptions({ headers: headers });
return this.http
.post(`${this.baseUrl}/SaveTicket`, formData, options)
.map(this.extractData)
.catch(this.handleError);
یک نکته: در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت میکنید.
تکمیل کامپوننت ارسال درخواست پشتیبانی
پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفادهی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازندهی UploadFileSimpleComponent تزریق میکنیم:
import { UploadFileSimpleService } from "./../upload-file-simple.service";
export class UploadFileSimpleComponent implements OnInit {
constructor(private uploadService: UploadFileSimpleService ) {}
و سپس متد submitForm چنین شکلی را پیدا میکند:
submitForm(form: NgForm) {
const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
console.log("fileInput.files", fileInput.files);
this.uploadService
.postTicket(this.model, fileInput.files)
.subscribe(data => {
console.log("success: ", data);
});
}
در اینجا this.model حاوی اطلاعات شیء ticket است (برای مثال اطلاعات توضیحات آن) و fileInput.files امکان دسترسی به اطلاعات فایلهای انتخابی توسط کاربر را میدهد. پس از آن فراخوانی متدهای this.uploadService.postTicket و subscribe، سبب ارسال این اطلاعات به سمت سرور میشوند.
دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیرهی فایلهای آن
کدهای کامل SimpleUpload که در سرویس فوق مشخص شدهاست، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شدهاست:
namespace AngularTemplateDrivenFormsLab.Models
{
public class Ticket
{
public int Id { set; get; }
public string Description { set; get; }
}
}
و سپس کنترلر ذخیره سازی اطلاعات Ticket را مشاهده میکنید:
using System.IO;
using System.Threading.Tasks;
using AngularTemplateDrivenFormsLab.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
namespace AngularTemplateDrivenFormsLab.Controllers
{
[Route("api/[controller]")]
public class SimpleUploadController : Controller
{
private readonly IHostingEnvironment _environment;
public SimpleUploadController(IHostingEnvironment environment)
{
_environment = environment;
}
[HttpPost("[action]")]
public async Task<IActionResult> SaveTicket(Ticket ticket)
{
//TODO: save the ticket ... get id
ticket.Id = 1001;
var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsRootFolder))
{
Directory.CreateDirectory(uploadsRootFolder);
}
var files = Request.Form.Files;
foreach (var file in files)
{
//TODO: do security checks ...!
if (file == null || file.Length == 0)
{
continue;
}
var filePath = Path.Combine(uploadsRootFolder, file.FileName);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(fileStream).ConfigureAwait(false);
}
}
return Created("", ticket);
}
}
}
توضیحات تکمیلی
- تزریق IHostingEnvironment در سازندهی کلاس کنترلر، سبب میشود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایلهای نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه میکنید، هنوز هم model binding کار کرده و میتوان شیء Ticket را به نحو متداولی دریافت کرد:
SaveTicket(Ticket ticket)
اما همانطور که عنوان شد، چون در حلقهی افزودن فایلها در سمت کلاینت، کلید نام این فایلها هربار متفاوت است:
formData.append(filesList[i].name, filesList[i]);
مجبور هستیم در سمت سرور بر روی Request.Form.Files یک حلقه را تشکیل داده و تمام فایلهای رسیده را پردازش کنیم:
var files = Request.Form.Files;
foreach (var file in files)
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.