假設今天有個狀況是這樣:有一筆日誌,新增第二筆但還沒送出前,想將第一筆刪除,這時會發生什麼事呢?
竟然出錯了!明明只是將要刪除的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