[ตอนที่ 11] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 Upload Photos

Line
Facebook
Twitter
Google

สารบัญเนื้อหา

  1. จัดเก็บรูปภาพที่ไหนได้บ้างและควรเลือกใช้ที่ไหน
  2. การใช้งาน Cloudinary เพื่อจัดเก็บรูปภาพ
  3. สร้าง Photos Controller
  4. ทดสอบ Upload Photo ด้วย Postman
  5. สร้าง Photo Component เพื่อ Upload Photos จาก Angular 9
  6. ใช้งาน 3rd Party ช่วยในการอัพโหลด
  7. กำหนดภาพเป็นภาพโปรไฟล์จาก Photos
  8. เพิ่มการแสดงรูปภาพโปรไฟล์บนเมนู
  9. ใช้งาน Behavior Subject เพื่อสร้าง Component แบบสื่อสารหลายทิศทาง (any to any communication)
  10. การลบรูปภาพที่อัพโหลด

1. จัดเก็บรูปภาพที่ไหนได้บ้างและควรเลือกใช้ที่ไหน

เราสามารถอัพโหลดและจัดเก็บไฟล์หรือรูปภาพจากระบบที่ไหนได้บ้าง และเราควรใช้ที่ไหน ซึ่งก็มีอยู่ 3 ทางเลือก คือ

  • Database
  • File System
  • Cloud

ถ้าเราจัดเก็บไฟล์ใน Database ก็สามารถทำได้ แต่จะทำให้ Database เราใหญ่และเพิ่มขึ้นอย่างรวดเร็วมาก วิธีการนี้จึงไม่ค่อยได้รับความนิยม เพราะเรื่องของประสิทธิภาพและอาการบวมของ Database

ถ้าจัดเก็บไฟล์ใน File System ก็สามารถทำได้ แต่ Server เราก็ต้องมีพื้นที่รองรับการจัดเก็บไฟล์เช่นเดียวกัน ซึ่งก็ตามมาด้วยภาระในการบริหารจัดการที่มากขึ้นด้วย ซึ่งในกรณีนี้เหมาะสำหรับใช้จัดเก็บไฟล์ในองค์ที่มีทรัพยากรของตนเองอยู่แล้ว และไม่ต้องการให้ข้อมูลถูกจัดเก็บอยู่นอกหน่วยงาน

ถ้าจัดเก็บไฟล์ใน Cloud ก็เป็นอีกหนึ่งทางเลือกที่ทำได้ ซึ่งแนวทางนี้เป็นที่ได้รับความนิยมสูงและมีประสิทธิภาพมากในปัจจุบัน เนื่องจากลดภาระของผู้ดูแลระบบไปส่วนหนึ่ง อีกทั้งไม่เปลือง Resource ด้วย แต่ถ้าใช้เยอะ ๆ ก็จะต้องมีค่าใช้จ่ายที่เพิ่มตามลำดับเช่นกัน ถือได้ว่าช่องทางนี้เหมาะสำหรับผู้ประกอบการรายย่อยและผู้ที่ต้องการประสิทธิภาพสูงแต่ไม่ต้องการรับภาระเอง

ในที่นี้จะใช้ Cloudinary ซึ่งเป็น Cloud เจ้าหนึ่งที่สามารถใช้ฟรีได้ในระยะเริ่มต้น ถ้าใช้ไม่เกินตามที่กำหนดก็ไม่ต้องเสียตังค์ รายละเอียดติดตามได้ที่ cloudinary.com

2. การใช้งาน Cloudinary เพื่อจัดเก็บรูปภาพ

ทำการสมัครและรับข้อมูลที่ต้องใช้ที่ https://cloudinary.com/users/register/free นำ CloudName, ApiKey และ ApiSecret มาจัดเก็บและเรียกใช้ใน API โดยกำหนดค่าไว้ที่ appsettings.json

{
  ...,
  "CloudinarySettings": {
    "CloudName": "isamrid",
    "ApiKey": "567962829459424",
    "ApiSecret": "p_NeFViCFiIXJTDZoleXzpAyt2M"
  }
}

สร้าง CloudinarySettings.cs ใน Helpers

namespace SocialApp_API.Helpers
{
    public class CloudinarySettings
    {
        public string CloudName { get; set; }
        public string ApiKey { get; set; }
        public string ApiSecret { get; set; }
    }
}

เพิ่ม Services ใน Startup.cs

services.Configure<CloudinarySettings>(Configuration.GetSection("CloudinarySettings"));

เพิ่ม PublicId ใน Models Photo.cs

public string PublicId { get; set; }

ทำการ Add Migrations และ Update Database

dotnet ef migrations add AddPublicId
dotnet ef database update

เพิ่ม Package CloudinaryDotNet

CloudinaryDotNet

3. สร้าง Photos Controller

สร้าง PhotoForCreationDto

using System;
using Microsoft.AspNetCore.Http;

namespace SocialApp_API.Dtos
{
    public class PhotoForCreationDto
    {
        public string Url { get; set; }
        public IFormFile File { get; set; }
        public string Description { get; set; }
        public DateTime DateAdded { get; set; }
        public string PublicId { get; set; }
        public PhotoForCreationDto()
        {
            DateAdded = DateTime.Now;
        }
    }
}

สร้าง Controller PhotosController.cs

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoMapper;
using CloudinaryDotNet;
using CloudinaryDotNet.Actions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using SocialApp_API.Data;
using SocialApp_API.Dtos;
using SocialApp_API.Helpers;
using SocialApp_API.Models;

namespace SocialApp_API.Controllers
{
    [Authorize]
    [Route("api/users/{userId}/photos")]
    [ApiController]
    public class PhotosController : ControllerBase
    {
        private readonly ISocialRepository _repo;
        private readonly IMapper _mapper;
        private readonly IOptions<CloudinarySettings> _cloudinaryConfig;
        private Cloudinary _cloudinary;
        public PhotosController(ISocialRepository repo, IMapper mapper, IOptions<CloudinarySettings> cloudinaryConfig)
        {
            _cloudinaryConfig = cloudinaryConfig;
            _mapper = mapper;
            _repo = repo;

            Account acc = new Account(
                _cloudinaryConfig.Value.CloudName,
                _cloudinaryConfig.Value.ApiKey,
                _cloudinaryConfig.Value.ApiSecret
            );

            _cloudinary = new Cloudinary(acc);
        }

        [HttpPost]
        public async Task<IActionResult> AddPhotoForUser(int userId, [FromForm]PhotoForCreationDto photoForCreationDto)
        {
            if (id != int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value))
                return Unauthorized();

            var userFromRepo = await _repo.GetUser(userId);

            var file = photoForCreationDto.File;
            var uploadResult = new ImageUploadResult();
            if (file.Length > 0)
            {
                using (var stream = file.OpenReadStream())
                {
                    var uploadParams = new ImageUploadParams()
                    {
                        File = new FileDescription(file.Name, stream),
                        Transformation = new Transformation()
                            .Width(500).Height(500).Crop("fill").Gravity("face")
                    };

                    uploadResult = _cloudinary.Upload(uploadParams);
                }
            }

            photoForCreationDto.Url = uploadResult.Url.ToString();
            photoForCreationDto.PublicId = uploadResult.PublicId;

            var photo = _mapper.Map<Photo>(photoForCreationDto);

            if (!userFromRepo.Photos.Any(u => u.isMain))
                photo.isMain = true;

            userFromRepo.Photos.Add(photo);

            if (await _repo.SaveAll())
            {
                return Ok();
            }

            return BadRequest("Cloud not add the photo");
            
        }
    }
}

เพิ่ม PhotoForReturnDto.cs

using System;

namespace SocialApp_API.Dtos
{
    public class PhotoForReturnDto
    {
        public int Id { get; set; }
        public string Url { get; set; }
        public string Description { get; set; }
        public DateTime DateAdded { get; set; }
        public bool isMain { get; set; }
        public string PublicId { get; set; }
    }
}

สร้าง Auto Mapper ใน AutoMapperProfiles.cs

CreateMap<Photo, PhotoForReturnDto>();
CreateMap<PhotoForCreationDto, Photo>();

เพิ่ม GetPhoto ใน ISocialRepository.cs

Task<Photo> GetPhoto(int id);

ทำการ implement interface ใน SocialRepository.cs

public async Task<Photo> GetPhoto(int id)
{
    var photo = await _context.Photos.FirstOrDefaultAsync(p => p.Id == id);

    return photo;
}

เพิ่ม GetPhoto() ใน PhotosControllers.cs

[HttpGet("{id}", Name = "GetPhoto")]
public async Task<IActionResult> GetPhoto(int id)
{
    var photoFromRepo = await _repo.GetPhoto(id);
    
    var photo = _mapper.Map<PhotoForReturnDto>(photoFromRepo);

    return Ok(photo);
}

ปรับปรุง AddPhotoForUser() ใน PhotosControllers.cs

if (await _repo.SaveAll())
{
    var photoToReturn = _mapper.Map<PhotoForReturnDto>(photo);
    return CreatedAtRoute("GetPhoto", new { userId = userId, id = photo.Id }, photoToReturn);
}
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoMapper;
using CloudinaryDotNet;
using CloudinaryDotNet.Actions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using SocialApp_API.Data;
using SocialApp_API.Dtos;
using SocialApp_API.Helpers;
using SocialApp_API.Models;

namespace SocialApp_API.Controllers
{
    [Authorize]
    [Route("api/users/{userId}/photos")]
    [ApiController]
    public class PhotosController : ControllerBase
    {
        private readonly ISocialRepository _repo;
        private readonly IMapper _mapper;
        private readonly IOptions<CloudinarySettings> _cloudinaryConfig;
        private Cloudinary _cloudinary;
        public PhotosController(ISocialRepository repo, IMapper mapper, IOptions<CloudinarySettings> cloudinaryConfig)
        {
            _cloudinaryConfig = cloudinaryConfig;
            _mapper = mapper;
            _repo = repo;

            Account acc = new Account(
                _cloudinaryConfig.Value.CloudName,
                _cloudinaryConfig.Value.ApiKey,
                _cloudinaryConfig.Value.ApiSecret
            );

            _cloudinary = new Cloudinary(acc);
        }

        [HttpGet("{id}", Name = "GetPhoto")]
        public async Task<IActionResult> GetPhoto(int id)
        {
            var photoFromRepo = await _repo.GetPhoto(id);
            
            var photo = _mapper.Map<PhotoForReturnDto>(photoFromRepo);

            return Ok(photo);
        }

        [HttpPost]
        public async Task<IActionResult> AddPhotoForUser(int userId, [FromForm]PhotoForCreationDto photoForCreationDto)
        {
            if (userId != int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value))
                return Unauthorized();

            var userFromRepo = await _repo.GetUser(userId);

            var file = photoForCreationDto.File;

            var uploadResult = new ImageUploadResult();
            
            if (file.Length > 0)
            {
                using (var stream = file.OpenReadStream())
                {
                    var uploadParams = new ImageUploadParams()
                    {
                        File = new FileDescription(file.Name, stream),
                        Transformation = new Transformation()
                            .Width(500).Height(500).Crop("fill").Gravity("face")
                    };

                    uploadResult = _cloudinary.Upload(uploadParams);
                }
            }

            photoForCreationDto.Url = uploadResult.Url.ToString();
            photoForCreationDto.PublicId = uploadResult.PublicId;

            var photo = _mapper.Map<Photo>(photoForCreationDto);

            if (!userFromRepo.Photos.Any(u => u.isMain))
                photo.isMain = true;

            userFromRepo.Photos.Add(photo);

            if (await _repo.SaveAll())
            {
                var photoToReturn = _mapper.Map<PhotoForReturnDto>(photo);
                return CreatedAtRoute("GetPhoto", new { userId = userId, id = photo.Id }, photoToReturn);
            }

            return BadRequest("Cloud not add the photo");
        }
    }
}

4. ทดสอบ Upload Photo ด้วย Postman

ทดสอบ Upload Photo ไปที่ http://localhost:5000/api/users/1/photos

ใส่ Token ให้เรียบร้อย

ใส่ Body เป็น form-data -> File (File) -> เลือกไฟลภาพ แล้วกด Send

5. สร้าง Photo Component เพื่อ Upload Photos จาก Angular 9

สร้าง Photo.cs ใน _model

export interface Photo {
    id: number;
    url: string;
    description: string;
    dateAdded: Date;
    isMain: boolean;
}

สร้าง Component photo-editor ใน Members แล้วทำการ imports

photo-editor.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Photo } from 'src/app/_models/photo';

@Component({
  selector: 'app-photo-editor',
  templateUrl: './photo-editor.component.html',
  styleUrls: ['./photo-editor.component.css']
})
export class PhotoEditorComponent implements OnInit {
  @Input() photos: Photo[];

  constructor() { }

  ngOnInit() {
  }

}

photo-editor.component.html

<div class="row">
  <div class="col-sm-2" *ngFor="let photo of photos">
    <img src="{{photo.url}}" class="img-thumbnail p-1">
    <div class="text-center">
      <button type="button" class="btn btn-sm">Main</button>
      <button type="button" class="btn btn-sm btn-danger"><i class="fa fa-trash-o"></i></button>
    </div>
  </div>
</div>

photo-editor.component.css

.img.img-thumbnail {
    height: 100px;
    min-width: 100px !important;
    margin-bottom: 2px;
}

ใช้งาน photo-editor component ใน member-edit component ในส่วนของการแก้ไขรูปภาพ

<app-photo-editor [photos]="user.photos"></app-photo-editor>

6. ใช้งาน 3rd Party ช่วยในการอัพโหลด

ติดตั้ง package ng2-file-upload เพื่อช่วยในการอัพโหลดไฟล์

npm install ng2-file-upload --save

import FileUploadModule ใน app.module.ts

import { FileUploadModule } from 'ng2-file-upload';

Initial file upload ใน photo-editor.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { FileUploader } from 'ng2-file-upload';
import { Photo } from 'src/app/_models/photo';
import { environment } from '../../../environments/environment';
import { AuthService } from 'src/app/_services/auth.service';

@Component({
  selector: 'app-photo-editor',
  templateUrl: './photo-editor.component.html',
  styleUrls: ['./photo-editor.component.css']
})
export class PhotoEditorComponent implements OnInit {
  @Input() photos: Photo[];
  uploader: FileUploader;
  hasBaseDropZoneOver: false;
  baseUrl = environment.apiUrl;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.initializeUpload();
  }

  fileOverBase(e: any): void {
    this.hasBaseDropZoneOver = e;
  }

  initializeUpload() {
    this.uploader = new FileUploader({
      url: this.baseUrl + 'users/' + this.authService.decodeToken.nameid + '/photos',
      authToken: 'Bearer ' + localStorage.getItem('token'),
      isHTML5: true,
      allowedFileType: ['image'],
      removeAfterUpload: true,
      autoUpload: false,
      maxFileSize: 10 * 1024 * 1024
    });

    this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; };

    this.uploader.onSuccessItem = (item, response, status, headers) => {
      if (response) {
        const res: Photo = JSON.parse(response);
        const photo = {
          id: res.id,
          url: res.url,
          dateAdded: res.dateAdded,
          description: res.description,
          isMain: res.isMain
        };
        this.photos.push(photo);
      }
    };

  }

}

สร้าง interface สำหรับการอัพโหลดไฟล์ใน photo-editor.component.html

<div class="row">
  <div class="col-sm-2" *ngFor="let photo of photos">
    <img src="{{photo.url}}" class="img-thumbnail p-1">
    <div class="text-center">
      <button type="button" class="btn btn-sm">Main</button>
      <button type="button" class="btn btn-sm btn-danger"><i class="fa fa-trash-o"></i></button>
    </div>
  </div>
</div>

<div class="row mt-3">
 
  <div class="col-md-3">

      <h3>Add Photos</h3>

      <div ng2FileDrop
           [ngClass]="{'nv-file-over': hasBaseDropZoneOver}"
           (fileOver)="fileOverBase($event)"
           [uploader]="uploader"
           class="card bg-faded p-3 text-center mb-3 my-drop-zone">
           <i class="fa fa-upload fa-3x"></i>
          Drop Photos Here
      </div>

      Multiple
      <input type="file" ng2FileSelect [uploader]="uploader" multiple  /><br/>

      Single
      <input type="file" ng2FileSelect [uploader]="uploader" />
  </div>

  <div class="col-md-9" style="margin-bottom: 40px" *ngIf="uploader?.queue?.length">

      <h3>Upload queue</h3>
      <p>Queue length: {{ uploader?.queue?.length }}</p>

      <table class="table">
          <thead>
          <tr>
              <th width="50%">Name</th>
              <th>Size</th>
          </tr>
          </thead>
          <tbody>
          <tr *ngFor="let item of uploader.queue">
              <td><strong>{{ item?.file?.name }}</strong></td>
              <td *ngIf="uploader.options.isHTML5" nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td>
          </tr>
          </tbody>
      </table>

      <div>
          <div>
              Queue progress:
              <div class="progress mb-4">
                  <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': uploader.progress + '%' }"></div>
              </div>
          </div>
          <button type="button" class="btn btn-success btn-s"
                  (click)="uploader.uploadAll()" [disabled]="!uploader.getNotUploadedItems().length">
              <span class="fa fa-upload"></span> Upload
          </button>
          <button type="button" class="btn btn-warning btn-s"
                  (click)="uploader.cancelAll()" [disabled]="!uploader.isUploading">
              <span class="fa fa-ban"></span> Cancel
          </button>
          <button type="button" class="btn btn-danger btn-s"
                  (click)="uploader.clearQueue()" [disabled]="!uploader.queue.length">
              <span class="fa fa-trash"></span> Remove
          </button>
      </div>

  </div>

</div>

photo-editor.component.css

.img.img-thumbnail {
    height: 100px;
    min-width: 100px !important;
    margin-bottom: 2px;
}

.nv-file-over {
    border: dotted 3px red;
}

input[type=file] {
    color: transparent;
}

ทดสอบรันและลองอัพโหลดภาพ

ถ้าพบว่ามี error เกิดขึ้นเกี่ยวกับ CORs อย่าลืมเพิ่มโค้ด

this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; };

7. กำหนดภาพเป็นภาพโปรไฟล์จาก Photos

สร้าง Interface GetMainPhotoForUser ใน ISocialRepository.cs

Task<Photo> GetMainPhotoForUser(int userId);

ทำการ Implement Interface GetMainPhotoForUser ใน SocialRepository.cs

public async Task<Photo> GetMainPhotoForUser(int userId)
{
    return await _context.Photos.Where(u => u.UserId == userId)
        .FirstOrDefaultAsync(p => p.isMain);
}

สร้าง Method SetMainPhoto ใน PhotoController.cs

[HttpPost("{id}/setMain")]
public async Task<IActionResult> SetMainPhoto(int userId, int id)
{
    if (userId != int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value))
        return Unauthorized();

    var user = await _repo.GetUser(userId);

    if (!user.Photos.Any(p => p.Id == id))
        return Unauthorized();

    var photoFromRepo = await _repo.GetPhoto(id);

    if (photoFromRepo.isMain)
        return BadRequest("This is already the main photo");
    
    var currentMainPhoto = await _repo.GetMainPhotoForUser(userId);
    currentMainPhoto.isMain = false;

    photoFromRepo.isMain = true;

    if (await _repo.SaveAll())
        return NoContent();
    
    return BadRequest("Cloud not set photo to main");
}

เพิ่ม Service ใน user.service.ts

setMainPhoto(userId: number, id: number) {
  return this.http.post(this.baseUrl + 'users/' + userId + '/photos/' + id + '/setMain', {});
}

เพิ่มฟังก์ชัน setMainPhoto ใน photo-editor.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { FileUploader } from 'ng2-file-upload';
import { Photo } from 'src/app/_models/photo';
import { environment } from '../../../environments/environment';
import { AuthService } from 'src/app/_services/auth.service';
import { UserService } from 'src/app/_services/user.service';
import { AlertifyService } from 'src/app/_services/alertify.service';

@Component({
  selector: 'app-photo-editor',
  templateUrl: './photo-editor.component.html',
  styleUrls: ['./photo-editor.component.css']
})
export class PhotoEditorComponent implements OnInit {
  @Input() photos: Photo[];
  uploader: FileUploader;
  hasBaseDropZoneOver: false;
  baseUrl = environment.apiUrl;
  currentMain: Photo;

  constructor(private authService: AuthService, private userService: UserService, private alertify: AlertifyService) { }

  ngOnInit() {
    this.initializeUpload();
  }

  fileOverBase(e: any): void {
    this.hasBaseDropZoneOver = e;
  }

  initializeUpload() {
    this.uploader = new FileUploader({
      url: this.baseUrl + 'users/' + this.authService.decodeToken.nameid + '/photos',
      authToken: 'Bearer ' + localStorage.getItem('token'),
      isHTML5: true,
      allowedFileType: ['image'],
      removeAfterUpload: true,
      autoUpload: false,
      maxFileSize: 10 * 1024 * 1024
    });

    this.uploader.onAfterAddingFile = (file) => { file.withCredentials = false; };

    this.uploader.onSuccessItem = (item, response, status, headers) => {
      if (response) {
        const res: Photo = JSON.parse(response);
        const photo = {
          id: res.id,
          url: res.url,
          dateAdded: res.dateAdded,
          description: res.description,
          isMain: res.isMain
        };
        this.photos.push(photo);
      }
    };

  }

  setMainPhoto(photo: Photo) {
    this.userService.setMainPhoto(this.authService.decodeToken.nameid, photo.id).subscribe(() => {
      this.currentMain = this.photos.filter(p => p.isMain === true)[0];
      this.currentMain.isMain = false;
      photo.isMain = true;
    }, error => {
      this.alertify.error(error);
    });
  }

}

ปรับปรุงปุ่ม Main ใน photo-editor.component.html

<div class="text-center mt-1 mb-1">
      <button type="button" class="btn btn-sm mr-1" 
      (click)="setMainPhoto(photo)" 
      [ngClass]="photo.isMain ? 'btn-success active' : 'btn-secondary'"
      [disabled]="photo.isMain">Main</button>

ทดสอบรัน และกำหนดภาพเป็นภาพโปรไฟล์ และลอง Refresh ดูผลการเปลี่ยนแปลง จะเห็นว่าภาพโปรไฟล์เปลี่ยนแล้ว แต่ยังไม่เปลี่ยนทันทีจนกว่าเราจะ Refresh

ทำการเพิ่ม Output ใน photo-editor.component.ts เพื่อส่งออกไปยัง photo-edit.component

@Output() getMemberPhotoChange = new EventEmitter<string>();

เพิ่มการส่งค่า Output ใน setMainPhoto

setMainPhoto(photo: Photo) {
  this.userService.setMainPhoto(this.authService.decodeToken.nameid, photo.id).subscribe(() => {
    this.currentMain = this.photos.filter(p => p.isMain === true)[0];
    this.currentMain.isMain = false;
    photo.isMain = true;
    this.getMemberPhotoChange.emit(photo.url);
  }, error => {
    this.alertify.error(error);
  });
}

เพิ่มการรับค่า Output ใน app-photo-editor ใน member-edit.component.html

<app-photo-editor [photos]="user.photos" (getMemberPhotoChange)="updateMainPhoto($event)"></app-photo-editor>

เพิ่มฟังก์ชันการอัพเดตภาพโปรไฟล์ใน member-edit.component.ts

updateMainPhoto(photoUrl) {
  this.user.photoUrl = photoUrl;
}

ลองรันและทดสอบ คราวนี้ภาพโปรไฟล์จะเปลี่ยยนทันทีแล้ว

8. เพิ่มการแสดงรูปภาพโปรไฟล์บนเมนู

ก่อนที่จะนำรูปภาพโปรไฟล์มาแสดงบนเมนูได้ จะต้องมีข้อมูลของ user ที่กำลัง login ก่อน แต่ตอนนี้เรายังไม่มีข้อมูลชุดที่จะสามารถเรียกได้ตลอดเวลา เราจึงต้องทำการสร้างชุดข้อมูลของ user ที่ login โดยจัดเก็บไว้ที่ localStorage

ในส่วนของ API เพิ่มการส่งข้อมูล user จากการ login

เพิ่มการจัดเก็บข้อมูล user ที่ login ใน localStorage ใน LoginController.cs ใน Method Login

var user = _mapper.Map<UserForListDto>(userFromRepo);

return Ok(new
{
    token = tokenHandler.WriteToken(token),
    user
});

แก้ไข Login() ใน AuthRepository.cs เพื่อให้ได้ url ของ photo ของ user นั้น ๆ

var user = await _context.Users.Include(p => p.Photos).FirstOrDefaultAsync(x => x.Username == username);

ในส่วนของ Client เพิ่มการจัดเก็บข้อมูล user ใน localStorage

เพิ่มการจัดเก็บ user ใน localStorage ที่ login ของ auth.service.ts

login(model: any) {
  return this.http.post(this.baseUrl + 'login', model).pipe(
    map((response: any) => {
      const user = response;
      if (user) {
        localStorage.setItem('token', user.token);
        localStorage.setItem('user', JSON.stringify(user.user));
        this.decodeToken = this.jwtHelper.decodeToken(user.token);
        this.currentUser = user.user;
      }
    })
  );
}

เพิ่มการดึงข้อมูล user ใน onInit ใน app.module.ts

ngOnInit() {
  const token = localStorage.getItem('token');
  const user: User = JSON.parse(localStorage.getItem('user'));
  if (token) {
    this.authService.decodeToken = this.jwtHelper.decodeToken(token);
  }

  if (user) {
    this.authService.currentUser = user;
  }
}

เพิ่มการเคลียร์ข้อมูลใน logout() ใน auth.service.ts

logout() {
  localStorage.removeItem('token');
  localStorage.removeItem('user');
  this.authService.decodeToken = null;
  this.authService.currentUser = null;
  this.alertify.message('Logged out');
  this.router.navigate(['/home']);
}

ทดลองรัน

แต่เมื่อลองเปลี่ยนภาพโปรไฟล์จะยังพบว่าภาพบนเมนูยังไม่เปลี่ยนตาม

9. ใช้งาน Behavior Subject เพื่อสร้าง Component แบบสื่อสารหลายทิศทาง (any to any communication)

import BehaviorSubject ใน auth.service.ts

import { BehaviorSubject } from 'rxjs';

เพิ่ม variable

photoUrl = new BehaviorSubject<string>('../../assets/user.png');
currentPhotoUrl = this.photoUrl.asObservable();

เพิ่ม changeMemberPhoto()

changeMemberPhoto(photoUrl: string) {
  this.photoUrl.next(photoUrl);
}

เรียกใช้ changeMemberPhoto() ใน login()

this.changeMemberPhoto(this.currentUser.photoUrl);

เพิ่ม variable ใน nav.component.ts

photoUrl: string;

เรียกใช้ค่าจาก currentPhotoUrl ใน onInit()

this.authService.currentPhotoUrl.subscribe(photoUrl => this.photoUrl = photoUrl);

เปลี่ยนค่าการแสดงภาพในเมนูจาก

<img src="{{authService.currentUser.photoUrl}}"/>

เป็น

<img src="{{photoUrl}}"/>

เพิ่มการเปลี่ยนค่า currentPhotoUrl โดยเรียกใช้ changeMemberPhoto() ใน app.component.ts

import { Component, OnInit } from '@angular/core';
import { JwtHelperService } from '@auth0/angular-jwt';
import { AuthService } from './_services/auth.service';
import { User } from './_models/user';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  jwtHelper = new JwtHelperService();

  constructor(private authService: AuthService){}

  ngOnInit() {
    const token = localStorage.getItem('token');
    const user: User = JSON.parse(localStorage.getItem('user'));
    if (token) {
      this.authService.decodeToken = this.jwtHelper.decodeToken(token);
    }

    if (user) {
      this.authService.currentUser = user;
      this.authService.changeMemberPhoto(user.photoUrl);
    }
  }
}

เพิ่ม var ใน member-edit.component.ts

photoUrl: string;

เพิ่มการเรียกใช้ photoUrl ใน onInit ใน member-edit.component.ts

this.authService.currentPhotoUrl.subscribe(photoUrl => this.photoUrl = photoUrl);

เรียกใช้ photoUrl ใหม่ ใน member-edit.component.html

<img src="{{photoUrl}}" alt="{{user.knownAs}}" class="card-img-top img-thumbnail">

เพิ่มการ set ค่า photoUrl ใน setMainPhoto() ใน member-editor.component.ts และยกเลิกการใช้ Output

setMainPhoto(photo: Photo) {
  this.userService.setMainPhoto(this.authService.decodeToken.nameid, photo.id).subscribe(() => {
    this.currentMain = this.photos.filter(p => p.isMain === true)[0];
    this.currentMain.isMain = false;
    photo.isMain = true;
    // this.getMemberPhotoChange.emit(photo.url);
    this.authService.changeMemberPhoto(photo.url);
    this.authService.currentUser.photoUrl = photo.url;
    localStorage.setItem('user', JSON.stringify(this.authService.currentUser));
  }, error => {
    this.alertify.error(error);
  });
}

ทดสอบรัน และลองเปลี่ยนภาพโปรไฟล์ จะพบว่าทุกจุดเปลี่ยนเหมือนกันทั้งหมดแล้ว

10. การลบรูปภาพที่อัพโหลด

เพิ่ม API สำหรับลบภาพใน PhotosController.cs

[HttpDelete("{id}")]
public async Task<IActionResult> DeletePhoto(int userId, int id)
{
    if (userId != int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value))
        return Unauthorized();

    var user = await _repo.GetUser(userId);

    if (!user.Photos.Any(p => p.Id == id))
        return Unauthorized();

    var photoFromRepo = await _repo.GetPhoto(id);

    if (photoFromRepo.isMain)
        return BadRequest("This is already the main photo");

    if (photoFromRepo.PublicId != null)
    {
        var deleteParams = new DeletionParams(photoFromRepo.PublicId);

        var result = _cloudinary.Destroy(deleteParams);

        if (result.Result == "ok") {
            _repo.Delete(photoFromRepo);
        }
    }

    if (photoFromRepo.PublicId == null)
    {
        _repo.Delete(photoFromRepo);
    }

    if (await _repo.SaveAll())
        return Ok();

    return BadRequest("Failed to delete the photo");
}

เพิ่ม deletePhoto() ใน user.service.ts

deletePhoto(userId: number, id: number) {
  return this.http.delete(this.baseUrl + 'users/' + userId + '/photos/' + id);
}

สร้างฟังก์ชันลบภาพใน photo-editor.component.ts

deletePhoto(id: number) {
  this.alertify.confirm("Are you sure you want to delete this photo?", () => {
    this.userService.deletePhoto(this.authService.decodeToken.nameid, id).subscribe(() => {
      this.photos.splice(this.photos.findIndex(p => p.id === id), 1);
      this.alertify.success('Photo has been deleted');
    }, error => {
      this.alertify.error(error);
    });
  });
}

เพิ่ม action click ให้ปุ่มลบภาพใน member-editor.component.html

<button type="button" class="btn btn-sm btn-danger" 
(click)="deletePhoto(photo.id)"
[disabled]="photo.isMain"><i class="fa fa-trash-o"></i></button>

ทดลองรันและลบภาพ

โปรดติดตามตอนต่อไป…

Line
Facebook
Twitter
Google
[ตอนที่ 16] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การทำ Sorting
[ตอนที่ 15] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การทำ Filtering
[ตอนที่ 14] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การใช้งาน Paging
No Preview
[ตอนที่ 13] Social App Workshop ด้วย ASP.NET Core 3 กับ Angular 9 การกำหนดรูปแบบการแสดงผลวัน-เวลา
เตรียม Atom สำหรับ React Native #3
เตรียม Visual Studio Code สำหรับ React Native #2
การติดตั้ง React Native บน macOs #1
การกำหนดค่า TF_MIN_GPU_MULTIPROCESSOR_COUNT เพื่อให้ TensorFlow ใช้งาน GPU ทุกตัว
ติดตั้ง Ubuntu 17.04 ใช้งานร่วมกับ Windows 10
การติดตั้ง TensorFlow & Caffe บน Ubuntu 16.04