ASP.NET MVC Ajax Add / Delete Image (MiniAlbum)

2022-01-10

ASP.NET MVC 使用 Ajax Helper 搭配 jQuery Ajax 實作非同步式圖片上傳與刪除。

logo

說明

  • Controller

    • Album
    • GetPhoto (retun partial)
    • AddPhoto (Post-only)
    • DeletePhoto (Post-only)
  • View

    • Album
    • GetPhoto (Partial)

Model

使用 App_Data 當中建立 mdf 的方式建立本次專案,並且搭配 SSMS 直接於 (localdb)\MSSQLLocalDB 進行資料庫的設計,僅有一張簡單的資料表負責圖片的上傳作業。

CREATE TABLE [dbo].[Photo](
	[Id] [int] IDENTITY(1,1) PRIMARY KEY NOT NULL,
	[FileName] [nvarchar](100) NOT NULL,
	[UpdateDateTime] [datetime2](7) NOT NULL,
	[MIME] [varchar](50) NOT NULL,
	[Path] [nvarchar](200) NOT NULL
)

Controller

Album

相簿的主頁,但 Controller 的工作卻相當簡單,只是負責回應 View。

public ActionResult Album()
{
    return View();
}

GetPhoto

Ajax 如果達成的靈魂 Action,負責提供 Partial View,提供 Ajax 呼叫所需的 Html 以進行頁面的更新。

public ActionResult GetPhoto()
{
    return PartialView(db.Photo.ToList());
}

AddPhoto

負責將收到的 HttpPostedFileBase 進行實體檔案的加入以及資料庫的內容寫入,因為是示範專案,許多上傳檔案需要進行的安全檢查沒有加入。

而回應的 ActionResult 為導向 GetPhoto,重新渲染 Photo。這樣雖然有利用到 Ajax 但還是處理了許多不需要更動的 HTML DOM ,在效能考量上不是最理想,精神上是參考 Ajax Helper Replace 的設計方式,好處就是設計上直覺且便利。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddPhoto(HttpPostedFileBase file)
{
    string extension = Path.GetExtension(file.FileName);
    string fileName = $"{Guid.NewGuid()}{extension}";
    string savePath = 
      Path.Combine(Server.MapPath("~/UploadFile"), fileName);
    file.SaveAs(savePath);

    db.Photo.Add(
        new Photo
        {
            FileName = fileName,
            Path = savePath,
            UpdateDateTime = DateTime.Now,
            MIME = MimeMapping.GetMimeMapping(savePath)
        }
    );

    db.SaveChanges();
    return RedirectToAction("GetPhoto", "Home");
}

DeletePhoto

public ActionResult DeletePhoto(int id)
{
    var photo = db.Photo.Find(id);
    if (photo != null)
    {
        if (System.IO.File.Exists(photo.Path))
        {
            System.IO.File.Delete(photo.Path);
        }

        db.Photo.Remove(photo);
        db.SaveChanges();
    }
    return RedirectToAction("GetPhoto", "Home");
}

View

解決 Bootstrap 3 Column Height 不一致問題

解決方法就是在 Row 加入 Flex,可以使用 inline-css:

<div class="row" style="display: flex; flex-wrap: wrap">
  <div class="col-md-2">
    <img src="..." class="img-responsive">
  </div>
</div>

元素較多的時候可以改採 CSS:

.equalHeight {
  display: flex; 
  flex-wrap: wrap
}

Album.cshtml

核心內容為 #album 的 Container 用於將 Partial View 鑲嵌於此

<div id="album">
    @Html.Action("GetPhoto")
</div>

搭配 Ajax ActionLink,可以用於非同步式地更新圖片顯示,本次範例中沒有使用到,但是使用 Ajax BeginForm 的前身,也方便在測試使用:

<div style="display: inline-block">
    @Ajax.ActionLink(
      "Update Photo", "GetPhoto", "Home", new { id = 0 },
      new AjaxOptions
      {
        HttpMethod = "GET",
        UpdateTargetId = "album",
        InsertionMode = InsertionMode.Replace
      },
      new { @class = "btn btn-primary", style = "margin-right: 5px" }
    )
</div>

實際使用的是 Ajax BeginForm,用於將上傳的圖片以 Form 的形式提供給 Home/AddPhoto Action

<div style="display: inline-block">
    @using (Ajax.BeginForm("AddPhoto", "Home", new { id = 0 },
        new AjaxOptions()
        {
            HttpMethod = "POST",
            UpdateTargetId = "album",
            InsertionMode = InsertionMode.Replace
        }, new { enctype = "multipart/form-data" }))
    {
        @Html.AntiForgeryToken()
        <input type="file" name="file" style="display: inline-block">
        <input type="submit" 
          value="Upload File to Server" 
          style="display: inline-block" 
          class="btn btn-primary">
    }
</div>

Script Section 有兩大功能:

第一個是負責將 jquery.unobtrusive-ajax.js 函式庫載入,以使用 Ajax。

第二個功能則是負責在 document 註冊 click 事件,用於處理圖片被點擊後刪除的事件。

@section scripts{
  @Scripts.Render("~/Scripts/jquery.unobtrusive-ajax.min.js")
  <script>
    $(document).on('click', '.removePhoto', function (e) {
      $.ajax("/Home/DeletePhoto/" + this.id, {
        success: function (data) {
          $('#album').html(data);
        }
      });
      console.log(this.id);
    });
  </script>
}

因為使用 Ajax 的方式將 getPhoto 的結果取代 Html Dom,如果不是在 Document 上使用 on 來註冊 Listener 的話,Listener 會因為 Dom 被更新而消失。如下列的程式碼的錯誤示範。

下列為錯誤示範,只能夠執行一次,後續會因 Listener 消失而無法再次使用。

$('.removePhoto').click(function (e) {
    $.ajax("/Home/DeletePhoto/" + this.id, {
        success: function (data) {
            $('#album').html(data);
        }
    });
    console.log(this.id);
});

GetPhoto.cshtml

GetPhoto 的任務單純,就是負責渲染 Model 當中的圖片,有使用到 flex style 解決 Bootstrap 3 Column Height 不一致的問題。

@model IList<MiniAlbum.Models.Photo>

<div class="row" style="margin-top:25px; display: flex; flex-wrap: wrap">
    @foreach (var img in Model)
    {
        <div class="col-md-2">
            <a class="removePhoto" id="@img.Id">
                <img src="~/UploadFile/@img.FileName" class="img-responsive" />
            </a>
        </div>
    }
</div>

小結

本專案僅為練習 ASP.NET MVC 搭配 jQuery Unobtrusive Ajax,在小型的專案可以採用,以提升更佳的網站使用體驗。但如果要追求更理想的體驗與更複雜的應用情境,還是得仰賴前端框架使用,並以 WebAPI 分前、後端的方式進行開發。

而本專案對於圖片的上傳、刪除還有可以改進的地方,例如刪除前的警示,圖片的上傳與刪除安全性上的考量也需加入,以免衍生資安漏洞。

參考資料

圖片來源 Pokemondb

相關連結

專案位置 | dev.azure

ASP.NET MVC 從無到有打造一個應用系統

Visual Studio 入門教學