Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[iThome 第七屆鐵人賽 06] ViewModel的重要性

在這一篇我們將來看一下在寫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裡面的DbSetEntity作為Model,就是實際Table 對應的Entity作為強型別View的Model。

使用Entity Framework的Entity作為ViewModel好還是不好有點看個人的習慣,不過我個人覺的是不好。

用Entity作為ViewModel的壞處是什麼

基本上有以下幾個問題:

  1. Model Binding的時候有風險 - 這個其實在新版本的Scaffolding有提醒(舊版本沒有),但是如果沒有注意的話可能還是會有問題
  2. 如果有些值是多個DB Table組成,用純Entity就沒有辦法做到

Model Binding 有風險

我們先來看一下預設Scaffolding Create Post Action的片段:

image
預設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是由使用者輸入,會需要:

  1. 刪掉產生這個Property的HtmlHelper
  2. [Binding]的欄位刪掉那個Property
上面關於第一點,如果忘記還好發現,因為忘記刪掉,畫面會看到欄位。但是,第二個就不好發現了。因為只要不是惡意嘗試,更本不會注意到。

ViewModel就可以解決這個問題。同時,用ViewModel還符合OO裡面的封裝(Encapsulation)概念,只開放要使用的欄位。

如果顯示的值是復合形的值

這個意思是,假設我們要顯示的值是不同地方組合出來的,要用Entity做ViewModel顯然不適合,因為Entity是Entity Framework對應到DB Table,因此不能夠隨意變動裡面的Property。

我們用個簡單的例子,假設我需要顯示一個值是DisplayNameUrl兩個欄位的值,如果使用是Entity做ViewModel,我們只能夠在View裡面手動把兩個 Property做concatenate顯示。如果這個View多處需要顯示這個值,或是不同的View都需要顯示這個值,用concatenate的方式做顯示有個問題,如果有天要替換兩個結合的Property,只能用搜索替換的方式來修改,很容易改錯,如果使用ViewModel就能夠解決這個問題。

只要在ViewModel定義一個Property,這個Property是兩個Property concatenate的結果,顯示在View的時候,就用那個Property。之後如果要改值,只需要改Property,所有的 View顯示的就會一起改變。

講了這們多,我們就看一下ViewModel到底是什麼。

ViewModel的定義

其實ViewModel的定義有些細節上面會不同,因此我下面介紹的屬於我個人覺得比較適合ViewModel的定義。個人在使用上符合自己習慣即可。

首先,ViewModel就像它的名字一樣,用於顯示View的Model

而我這邊的ViewModel定義是:

  1. ViewModel只是簡單的POCO Class - 就像Entity Framework裡面的Entity一樣,只有Property
  2. ViewModel不定義邏輯 - 有些會把取得資料的邏輯或者一些商業邏輯寫在ViewModel。但是,我認為ViewModel應該越純粹越好,它只是資料的載體,方便我們把資料傳到 View而已,那些商業邏輯是屬於service layer或者Business layer
  3. 每一個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之間的轉換。以上面的例子,這個轉換的邏輯不好通用。假設,今天我別的地方也需要把LinkCreateViewModelLink 做轉換,我要怎麼共用這個轉換的邏輯?就算寫成通用方法,想想如果每一個ViewModel都寫一次,不是很浪費時間嗎?


針對這個問題,我們下一篇將介紹一個常用的套件,AutoMapper來解決。


結語


透過這一篇我們了解到ViewModel的好處並且為什麼使用ViewModel。同時我們也注意到了ViewModel使用上面的不便利性。


下一篇將會介紹AutoMapper,看看AutoMapper是如何解決這個問題。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus