假設今天有個狀況是這樣:有一條日誌,新增第二條但還沒提交前,想將第一條刪除,這時會發生什麼事呢?
竟然出錯了!明明只是將要刪除的Post.Id提交後端去,為什麼會有這樣的錯誤訊息?

這就要說到 C# 的特性了,C# 是物件導向 (Object-Oriented Programming, OOP) 語言,也就是說任何東西包括資料、方法都能變成物件,Blog、Post 就是一個個物件,除了物件這種參考型別,也有單純的 int、bool 等基礎型別。
(註:string 分類上是參考型別,但語法上卻是基礎型別,這是為了避免無數的 string 撐滿記憶體。)
基礎型別的意義是:兩個基礎型別之間的修改不會影響彼此。定義一個變數 int a = 0;,再定義 int b = a;,b 等於 0 這沒問題,這時候如果再賦值 b = 3;,a 跟 b 就不相等了,彼此間不會影響對方。下圖用 LINQPad 示範,Dump() 的意思是將該變數顯示在下方 Results 區塊,可以看到即便中間修改 b 的值,a 也不受影響。

參考型別則是:B 物件如果來自 A 物件,不論哪個物件修改,另一個就會跟著修改。可以看到下圖在第 12 行將 B 物件的 Title 改為 "BB",結果 A 物件的 Title 也跟著變了。

那這些跟 Blog 有什麼關係呢?我們看後端 BlogRepository.cs 的 GetBlog(),可以看到這邊將 blog 回傳,前端 BlogBase.razor.cs 這邊接起來後,一旦觸發 Add() 就會在 Blog.Posts 新增一條 PostModel。

前端點擊 Delete 按鈕後,後端 PostRepository.cs 的 DeletePost() 這邊會觸發 SaveChanges(),這時候的 Blog.Posts 會有一條沒有 Blog、Title 跟 Content 的 PostModel,這條根本還沒點擊 Submit 按鈕經由後端存到資料庫,是只存在於前端的資料,但是觸發 SaveChanges() 的時候卻試圖將這條資料存進資料庫,Title 跟 Content 是不能為 null 的,自然就出錯了。


另外如果單純將資料庫的 Posts 取出來,是看不到那一條資料的,因為那是跟著 Blog 的 PostModel。

要解決這問題有幾種方法,第一種是將 Blog 跟 Post 完全拆開,兩者各有自己的前端頁面,不過如果現實情況的專案遇到這種坑(沒錯,這是筆者給自己挖的坑…),往往不會有時間做這種重構。
第二種方法是當後端 PostRepository.cs 收到沒有 Title 的 PostModel 時,回傳提示訊息。

前端 PostBase.razor.cs 修改為以 deleted.IsSuccess 判斷,刪除成功則將 Post!.Id 傳給 Blog 將該條 Post 從頁面刪除,失敗的話提示失敗的原因。


雖然以工程師的角度來看這樣避免了錯誤,但以 UX (User Experience) 角度來看根本就是莫名其妙,為什麼刪除一條日誌還要限制不能有空的日誌?所以就要用第三種方法。
第三種是建立 ViewModel,頁面的 CRUD 都針對 ViewModel 處理,之後才一一 Mapping 回去 Model。
所謂的 ViewModel 是指不存在於資料庫但又希望呈現在頁面上的欄位,例如有張 table Employee 裡面有兩個欄位 FirstName 跟 LastName,存進資料庫時分開存,但顯示時希望動些手腳(例如要組合起來且全大寫),可以把兩個欄位都丟到前端後再處理,由使用者的瀏覽器處理,也可以先在後端處理好再用 ViewModel 承接丟到前端。
另一個例子是信用卡,table CreditCard 存有使用者的信用卡號、三位數認證碼、出生年月日,大家應該常常網購,刷卡時會讓使用者看到信用卡末四碼,這種機密隱私資料總不可能 16 碼都丟到前端處理吧?這時就需要在後端處理後再由 ViewModel 傳到前端了。
我們先建立 BlogViewModel 跟 PostViewModel,因為是 ViewModel 所以不需要用跟資料庫相關的 [Key] attribute,有使用到 Model 的地方都改成 ViewModel。

接著修改後端 BlogRepository.cs,頁面呈現改成 ViewModel,資料存取沿用 Model,可以看到 36 到 56 行手動做 Mapping。


PostRepository.cs 的 CreatePost() 也是一樣,DeletePost() 則把原本的 else 區塊對 Blog.Posts 的判斷移除。


BlogBase.razor.cs 跟 PostBase.razor.cs 把原本用到的 Model 改成 ViewModel。

這時候來建立新資料,不過建立第二條後緊接著要刪除第二條,卻發生找不到 Post 的問題,這是為什麼?

原來第二條雖然進入資料庫了,但我們沒有重新將資料取回來,頁面的 Blog.Posts 第二條的 Post.Id 仍然是 0。

為了讓 Blog.Posts 知道要重取資料庫,我們要在 PostBase.razor.cs 新增 EventCallback,告知 BlogBase.razor.cs 再執行一次 LoadData(),因為是告知而已,就不用傳 <TValue>。



然後在新增第二條之後立刻刪除,就會正常了。新增第二條後再新增第三條,刪除第二條也會正常。
(註:如果看到下圖的錯誤訊息,有可能是 Visual Studio 的問題,先試試重新啟動 Visual Studio。)

引用:
- .NET Stack and Heap
- In C#, why is String a reference type that behaves like a value type?
- What is ViewModel in MVC?
- Understanding ViewModel in ASP.NET MVC
註:本文程式碼透過 .NET 6 + Visual Studio 2022 重構,可點擊原文連結與重構後程式碼比較學習,謝謝閱讀,支持原作者