首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET MVC5 文章管理:修改與刪除功能完整實作(第10篇)

     編輯:ASP.NET 基礎

前言

延續「文章列表」與「新增文章」,本篇實現修改與刪除,完成文章管理的 CRUD 閉環。重點包括:關聯實體刪除順序、TryUpdateModel 部分更新防參數綁定攻擊、附件清理邏輯。

一、刪除文章:關聯實體正確刪除順序

資料關係:CommonModel (1) ←→ (1) Article,CommonModel (1) ←→ (N) Attachment。
若先刪 CommonModel,外鍵約束會導致錯誤;必須先刪除子實體。

Repository 層:重寫 Delete 方法

public class CommonModelRepository : BaseRepository, ICommonModelRepository
{
    // 使用 new 關鍵字隱藏基類方法,實現級聯刪除附件
    public new bool Delete(CommonModel model, bool isSave = true)
    {
        if (model.Attachment != null && model.Attachment.Any())
            Context.Attachments.RemoveRange(model.Attachment);
        
        Context.CommonModels.Remove(model);
        return isSave ? Context.SaveChanges() > 0 : true;
    }
}

Controller:刪除 Action

public JsonResult Delete(int id)
{
    var article = _articleService.Find(id);
    if (article == null) return Json(false);

    var attachmentList = article.CommonModel.Attachment?.ToList();
    var commonModel = article.CommonModel;

    // 1. 先刪 Article(觸發 FK 級聯或手動處理)
    if (!_articleService.Delete(article)) return Json(false);

    // 2. 刪除實體檔案
    foreach (var att in attachmentList)
    {
        string physicalPath = Server.MapPath(att.FilePath);
        if (System.IO.File.Exists(physicalPath)) System.IO.File.Delete(physicalPath);
    }

    // 3. 刪除 CommonModel(Repository 已處理 Attachment 記錄)
    _commonModelService.Delete(commonModel);

    return Json(true);
}

二、修改文章:TryUpdateModel 白名單綁定

顯示編輯頁面

public ActionResult Edit(int id)
{
    var article = _articleService.Find(id);
    if (article == null) return HttpNotFound();
    return View(article);
}

編輯頁面 View(重點:KindEditor 整合、首頁圖片選擇)

@model Ninesky.Models.Article

@section Scripts {
    <script src="~/Scripts/KindEditor/kindeditor-min.js"></script>
    <script>
        KindEditor.ready(function (K) {
            // 內容編輯器
            window.editor = K.create('#Content', {
                height: '500px',
                uploadJson: '@Url.Action("Upload", "Attachment")',
                fileManagerJson: '@Url.Action("FileManagerJson", "Attachment", new { id = Model.CommonModel.ModelID })',
                allowFileManager: true
            });

            // 首頁圖片選擇器
            var editor2 = K.editor({
                fileManagerJson: '@Url.Action("FileManagerJson", "Attachment", new { id = Model.CommonModel.ModelID })'
            });
            K('#btnPicSelect').click(function () {
                editor2.loadPlugin('filemanager', function () {
                    editor2.plugin.filemanagerDialog({
                        viewType: 'VIEW', dirName: 'image',
                        clickFn: function (url, title) {
                            $.ajax({
                                type: "POST", url: '@Url.Action("CreateThumbnail", "Attachment")',
                                data: { originalPicture: url }, async: false,
                                success: function (data) {
                                    if (data) {
                                        K('#CommonModel_DefaultPicUrl').val(data);
                                        K('#imgPreview').attr("src", data);
                                    } else alert("縮圖生成失敗");
                                    editor2.hideDialog();
                                }
                            });
                        }
                    });
                });
            });
        });
    </script>
}

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <div class="form-horizontal">
        <h4>修改文章</h4> <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            @Html.LabelFor(m => m.CommonModel.CategoryID, new { @class = "control-label col-sm-2" })
            <div class="col-sm-10">
                <input id="CommonModel_CategoryID" name="CommonModel.CategoryID" 
                       data-options="url:'@Url.Action("JsonTree", "Category", new { model="Article" })'" 
                       class="easyui-combotree" style="height:34px;width:280px;" 
                       value="@Model.CommonModel.CategoryID" />
                @Html.ValidationMessageFor(m => m.CommonModel.CategoryID)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(m => m.CommonModel.Title, new { @class = "control-label col-sm-2" })
            <div class="col-sm-10">
                @Html.TextBoxFor(m => m.CommonModel.Title, new { @class = "form-control" })
                @Html.ValidationMessageFor(m => m.CommonModel.Title)
            </div>
        </div>

        @* Author, Source, Intro, Content 同樣處理 *@

        <div class="form-group">
            @Html.LabelFor(m => m.CommonModel.DefaultPicUrl, new { @class = "control-label col-sm-2" })
            <div class="col-sm-10">
                <img id="imgPreview" class="thumbnail" src="@Model.CommonModel.DefaultPicUrl" />
                @Html.HiddenFor(m => m.CommonModel.DefaultPicUrl)
                <a id="btnPicSelect" class="easyui-linkbutton">選擇…</a>
            </div>
        </div>

        <div class="form-group">
            <div class="col-sm-offset-2 col-sm-10">
                <input type="submit" value="儲存" class="btn btn-default" />
            </div>
        </div>
    </div>
}

接收修改:核心邏輯

[HttpPost]
[ValidateInput(false)]        // 允許 HTML 內容
[ValidateAntiForgeryToken]    // 防 CSRF
public ActionResult Edit()
{
    // 1. 從路由取得 ID
    int id = int.Parse(ControllerContext.RouteData.GetRequiredString("id"));
    var article = _articleService.Find(id);
    if (article == null) return HttpNotFound();

    // 2. 白名單部分更新:僅允許修改指定欄位
    TryUpdateModel(article, new[] { "Author", "Source", "Intro", "Content" });
    TryUpdateModel(article.CommonModel, "CommonModel", new[] { "CategoryID", "Title", "DefaultPicUrl" });

    if (ModelState.IsValid)
    {
        if (_articleService.Update(article))
        {
            // 3. 附件清理:檢查上傳但未使用的附件
            var attService = new AttachmentService();
            var uploads = attService.FindList(article.CommonModel.ModelID, User.Identity.Name, string.Empty, true).ToList();

            foreach (var att in uploads)
            {
                string fileUrl = Url.Content(att.FilePath);
                bool isUsed = (article.CommonModel.DefaultPicUrl?.Contains(fileUrl) ?? false) 
                           || article.Content.Contains(fileUrl);

                if (isUsed)
                {
                    att.ModelID = article.ModelID; // 歸屬給文章
                    attService.Update(att);
                }
                else
                {
                    // 刪除實體檔案與資料庫記錄
                    System.IO.File.Delete(Server.MapPath(att.FilePath));
                    attService.Delete(att);
                }
            }
            return View("EditSuccess", article);
        }
    }
    return View(article);
}

三、關鍵技術解析

技術點說明代碼位置
ValidateInput(false)關閉請求驗證,允許富文本 HTML 提交Edit Action
ValidateAntiForgeryToken防跨站請求偽造,配合 View @Html.AntiForgeryToken()Edit Action
TryUpdateModel 白名單僅綁定允許修改的屬性,防止惡意提交 CategoryID 等敏感欄位Edit Action
前綴綁定TryUpdateModel(model.CommonModel, "CommonModel", ...) 對應 View 中 name="CommonModel.Title"Edit Action
路由取 IDControllerContext.RouteData.GetRequiredString("id") 避免 Action 參數被竄改Edit Action
附件歸屬判斷檢查 DefaultPicUrl 與 Content 是否包含附件 URLEdit Action

四、我的文章列表

與「全部文章列表」類似,但:

  • Service 查詢固定 inputer = User.Identity.Name
  • 前端工具列簡化:僅保留修改、刪除、重新整理、搜尋
  • JS 邏輯復用:onDblClickRow 雙擊進入編輯、edit()/del() 函式

結語

文章管理 CRUD 至此完結。核心收穫:

  1. 刪除順序:子實體 → 父實體 → 實體檔案,避免 FK 約束衝突
  2. 部分更新:TryUpdateModel + 白名單是防參數綁定攻擊的標準做法
  3. 附件生命週期:上傳 → 暫存 → 文章儲存時歸屬 → 未使用則刪除

下一階段可延伸:版本控制、發布工作流、全文搜尋、多語系支援。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved