假設今天有個狀況是這樣:有一筆日誌,新增第二筆但還沒送出前,想將第一筆刪除,這時會發生什麼事呢?
竟然出錯了!明明只是將要刪除的PostId送到後端去,為什麼會有這樣的錯誤訊息?
這就要說到 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判斷,刪除成功則將PostId傳給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,可以看到 28 到 48 行手動做 Mapping。

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

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


這時候來建立新資料,不過建立第二筆後緊接著要刪除第二筆,卻發生找不到 Post的問題,這是為什麼?
原來第二筆雖然進入資料庫了,但我們沒有重新將資料撈回來,畫面的Blog.Posts第二筆的PostId仍然是0。
為了讓Blog.Posts知道要重撈資料庫,我們要在PostBase.razor.cs新增EventCallback,告知BlogBase.razor.cs再執行一次loadData(),因為是告知而已,就不用傳<TValue>。



然後在新增第二筆之後立刻刪除,就會正常了。新增第二筆後再新增第三筆,刪除第二筆也會正常。
(註:如果看到下圖的錯誤訊息,有可能是 Visual Studio 的問題,先試試重開Visual Studio。)
Ref: .NET Stack and Heap
Ref: In C#, why is String a reference type that behaves like a value type?
Ref: What is ViewModel in MVC?
Ref:Understanding ViewModel in ASP.NET MVC