在這一篇我們將來看一下在寫Mvc裡面最重要的一個概念,也就是強型別的View(Strong Type View)和ViewModel。
強型別的View(Strong Type View)
Mvc裡面的View使用的是Razor語法,而每一個View能夠定義傳進來的Model形態是什麼。透過這種方式,在寫View的時候不止能夠在Compile time的時候知道對於傳進來的Model使用上面有沒有正確,配上HtmlHelper,還能夠建立能夠和Model binding成功的對應html input。
強型別的View好處多多,但是問題在於這個型別應該要用什麼?如果用預設的Mvc做Scaffolding的話,預設用的是Entity Framework裡面的DbSet
Entity作為Model,就是實際Table 對應的Entity作為強型別View的Model。
使用Entity Framework的Entity作為ViewModel好還是不好有點看個人的習慣,不過我個人覺的是不好。
用Entity作為ViewModel的壞處是什麼
基本上有以下幾個問題:
- Model Binding的時候有風險 - 這個其實在新版本的Scaffolding有提醒(舊版本沒有),但是如果沒有注意的話可能還是會有問題
- 如果有些值是多個DB Table組成,用純Entity就沒有辦法做到
Model Binding 有風險
我們先來看一下預設Scaffolding Create Post Action的片段:
我們可以看到英文的註解,和[Bind(Include = "Id,DisplayName,Url")]
看到,其實它是在限制那些Property應該要做Model Binding。因此,以這個例子,Id、 DisplayName和Url將會做ModelBinding。
假設今天我們不希望Id是由使用者輸入,而是系統產生,因此我們會把畫面上面產生輸入Id的HtmlHelper拿掉。假設,使用者是透過我們的form表單post資訊回來,這個不會有問題。
但是,如果有人在Form post之前,增加一個欄位並且測出有Id這個欄位,那麼他post過來帶上Id這個欄位的是時候,我們存到DB值就錯了。
這個問題在於,假設今天我要刪除某一個Property是由使用者輸入,會需要:
- 刪掉產生這個Property的HtmlHelper
- 把
[Binding]
的欄位刪掉那個Property
ViewModel就可以解決這個問題。同時,用ViewModel還符合OO裡面的封裝(Encapsulation)概念,只開放要使用的欄位。
如果顯示的值是復合形的值
這個意思是,假設我們要顯示的值是不同地方組合出來的,要用Entity做ViewModel顯然不適合,因為Entity是Entity Framework對應到DB Table,因此不能夠隨意變動裡面的Property。
我們用個簡單的例子,假設我需要顯示一個值是DisplayName
和Url
兩個欄位的值,如果使用是Entity做ViewModel,我們只能夠在View裡面手動把兩個 Property做concatenate顯示。如果這個View多處需要顯示這個值,或是不同的View都需要顯示這個值,用concatenate的方式做顯示有個問題,如果有天要替換兩個結合的Property,只能用搜索替換的方式來修改,很容易改錯,如果使用ViewModel就能夠解決這個問題。
只要在ViewModel定義一個Property,這個Property是兩個Property concatenate的結果,顯示在View的時候,就用那個Property。之後如果要改值,只需要改Property,所有的 View顯示的就會一起改變。
講了這們多,我們就看一下ViewModel到底是什麼。
ViewModel的定義
首先,ViewModel就像它的名字一樣,用於顯示View的Model
而我這邊的ViewModel定義是:
- ViewModel只是簡單的POCO Class - 就像Entity Framework裡面的Entity一樣,只有Property
- ViewModel不定義邏輯 - 有些會把取得資料的邏輯或者一些商業邏輯寫在ViewModel。但是,我認為ViewModel應該越純粹越好,它只是資料的載體,方便我們把資料傳到 View而已,那些商業邏輯是屬於service layer或者Business layer
- 每一個View有自己的ViewModel - 每一個View應該要有自己對應的ViewModel
使用ViewModel如何解決上述的問題
Model Binding風險問題
我們可以用ViewModel來解決Model Binding的問題:
public class LinkCreateViewModel
{
public string DisplayName { get; set; }
public string Url { get; set; }
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(LinkCreateViewModel link)
{
if (ModelState.IsValid)
{
// 只設定需要的欄位
Link enity = new Link()
{
DisplayName = link.DisplayName,
Url = link.Url
};
db.Links.Add(enity);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(link);
}
這邊可以看到,透過ViewModel,我們只傳入實際讓使用者輸入的Property。假設那一天要減少一個Property,而轉換成為Entity的部分忘記改,在Compile的時候就會知道,這樣也可以減少bug的出現。假設還是用[Bind]
的方式,那麼減少欄位忘記改在compile是不會報錯,因為[Bind]
用的是string。
解決複合型資料問題
其實會和上面概念一樣,取得某一筆Entity資料之後,把它轉換成為ViewModel並且設定ViewModel Property顯示的值。然後,傳到View裡面。
ViewModel的問題
看完ViewModel的好處,我們也需要看一下它的問題。
最主要問題在於,Entity和ViewModel之間的轉換。以上面的例子,這個轉換的邏輯不好通用。假設,今天我別的地方也需要把LinkCreateViewModel
和Link
做轉換,我要怎麼共用這個轉換的邏輯?就算寫成通用方法,想想如果每一個ViewModel都寫一次,不是很浪費時間嗎?
針對這個問題,我們下一篇將介紹一個常用的套件,AutoMapper來解決。
結語
透過這一篇我們了解到ViewModel的好處並且為什麼使用ViewModel。同時我們也注意到了ViewModel使用上面的不便利性。
下一篇將會介紹AutoMapper,看看AutoMapper是如何解決這個問題。