2012年12月27日 星期四

ASP.NET MVC - 為什麼不建議在 ViewModel 裡加入行為

一開始必須再三地強調,這裡所說的 ViewModel 與 MVVM 所謂的 View Model 是不同的,所以 Sliverlight WPF  XAML 所會用到的 View Model 不在此篇文章的討論範圍內。

為什麼連兩篇有關 ViewModel 為主題的文章會一再地強調與 MVVM 的 View Model 有所區別,這是因為 MVVM 的 View Model 是要加入行為的,然而 MVC 的 ViewModel 卻是不建議加入。

因為看到有人把 MVVM 的 View Model 使用方式改拿到 MVC 來用,我覺得相當不妥,違反單一職責原則以及違背了 MVC 所強調的關注點分離,所以在一篇文章裡提出我之所以不建議在 ViewModel 裡加入行為(方法)的看法。

 


其實 ASP.NET MVC 的 ViewModel 真的會讓很多人困惑,因為相關的資訊不是很多,既使在 Google 有找到,但也絕大部分都是在說明 MVVM 的 View Model,以致於有些使用 ASP.NET MVC 的開發者會把 MVVM 的 View Model 作法也帶到了 MVC 來用。

在上一篇文章「ASP.NET MVC 的 ViewModel - 基礎篇」裡的後面有強調 ASP.NET MVC 的 ViewModel 不要加入行為,也就是不要加入方法;因為 ViewModel 只是單純用來給 View 呈現資料使用,所以不用加入任何的行為,ViewModel 就只是一個很單純的類別,一個只有資料成員的類別。

先看看之前文章中的例子,

CategoryController.cs

image

上圖的程式並沒有做分層架構,在 Controller 裡直接操作 EF 來取得資料,一般來說,都會將有關對 EF 做資料存取的操作給抽出來,或者是做得比較仔細的話,就會做分層架構(有關怎麼做分層架構,可以參考「分層架構」的系列文章),如此一來在 Controller 裡的 Action 方法就不會直接處理資料的存取,讓 Controller 只做該做的事情,以下我們以抽出 Repository 的方式來做說明。

 

Respository

所謂 Repository 就是把對 Model 資料的存取操作建立一個專屬的類別,而這個類別基本上會包含有 Create, Update, Delete, Read 等方法,每個 Respository 只會負責處理自己負責的 Model,不需要干涉其他的類別,現在我們分別建立 CategoryRespository 與 ProductRespository,Repository 會是屬於 Model 的範疇,

CategoryRepository.cs

image

ProductRepository.cs

image

在 Controller Action 方法內容也跟著做了修改,

image

這邊所做的改變是,Controller 不再有資料存取的責任,Controller 執行了要取什麼樣的資料的工作,而真正去資料庫取出資料的責任就交給了 Repository。

 

假如,有人覺得在 Controller Action 方法裡要分別去取得 Category, Products 資料很麻煩,而想要用個簡單的方法把取得 Category, Products 以及取得資料後放到 ViewModel 的這幾個部分給抽出來成為一個方法,並且將此方法加入到 CategoryDetailsViewModel 內,然後在建構式當中去取得屬性的資料,如下:

public class CategoryDetailsViewModel
{
    public Categories CategoryData
    {
        get; set;
    }
 
    public IEnumerable<Products> ProductCollection
    {
        get; set;
    }
 
    public CategoryDetailsViewModel()
    { 
    
    }
 
    public CategoryDetailsViewModel(int categoryID)
    {
        CategoryRepository categoryRepository = new CategoryRepository();
        ProductRepository productRepository = new ProductRepository();
 
        this.CategoryData = categoryRepository.Get(categoryID);
        this.ProductCollection = productRepository.GetByCategoryID(categoryID)
            .OrderBy(x => x.ProductID)
            .ToList();
    }
 
}

那麼原本 Controller Action 就變成以下的樣子,

image

這樣看起來好像蠻合理的,似乎這樣的方式好像也省下不少事情,只需要把接收到的 ID 值去建立一個 CategoryDetailsViewModel 實例,什麼要到哪裡取得資料,取得資料後要 bind 到 ViewModel 的哪一個屬性,這些事情就不用在 Controller Action 裡做,只要交給 ViewModel 就好了,而且也讓 Controller Action 方法內的程式碼減少許多,這樣的方式看起來好像一石二鳥, BUT …… 各位有沒有想過,當專案裡的 ViewModel 越來越多時,是不是每一個 ViewModel 類別都要這麼做呢?

也許有人認為,我只是不想所有的事情都擺在 Controller 裡面去做,只是把取得資料以及 Bind 資料到屬性的這些動作給移到 ViewModel 裡面而已,只是要讓 Controller 的程式碼看起來比較乾淨一些,在上面的例子看起來是有達到目的,這是因為取得的資料類別只有兩種,資料取得與 Bind 到 ViewModel 屬性的動作比較少:如果今天一個頁面所有呈現的資料會有十多種 Model 類別,而且取得資料的邏輯又極其複雜時,各位可以想一想,將這些處理資料的程式都放到 ViewModel 類別中是否合適。

放在 Controller 裡也不合適,因為過多繁雜的程式都擠在 Action 方法中,要關注處理的地方過多,這樣的程式不好維護,而且容易產生錯誤。

放在 Repository 裡更不合適,各個 Model 類別的 Repository 只會處理該 Model 類別的資料存取,CategoryRepository 只須負責 Category 資料的存取,不需要另外操心 Product 資料的存取。

那麼應該放在哪裡呢?

我的建議是可以另外建立服務層(Service),Service 層是服務展示層,將複雜並含有業務邏輯操作的程式移到 Service 層,是解決這樣的問題。

 

ViewModel 加入行為?

再用一個例子來做說明,這個例子裡,將會把資料更新的方法給寫在 ViewModel 內,

有兩個 Model 類別,分別為 MemberAccount 與 MemberDetail,

image

也分別為這兩個類別做了個別的 Repository,

MemberAccountRepository.cs

image

MemberDetailRepository.cs

image

 

建立一個 MemberDataViewModel.cs,有兩個屬性,分別為 MemberAccount 與 MemberDetail,另外把登入驗證、資料更新、資料建立的方法也都放在這個 viewmodel 裡,

MemberDataViewModel.cs

namespace ViewModelSample.ViewModels
{
    public class MemberDataViewModel
    {
        private MemberAccountRepository accountRepository = new MemberAccountRepository();
        private MemberDetailRepository detailRepository = new MemberDetailRepository();
 
        public MemberAccount Account
        {
            get;
            set;
        }
 
        public MemberDetail Detail
        {
            get;
            set;
        }
 
        public MemberDataViewModel()
        { 
        
        }
 
        public MemberDataViewModel(Guid id)
        {
            var accountData = accountRepository.Get(id);
            var detailData = detailRepository.GetByAccountID(id);
        }
 
        //=========================================================================================
        // Methods
 
        public void Create(MemberAccount account, MemberDetail detail)
        {
            this.accountRepository.Create(account);
            this.detailRepository.Create(detail);
        }
 
        public void Update(MemberAccount account, MemberDetail detail)
        {
            this.accountRepository.Create(account);
            this.detailRepository.Create(detail);
        }
 
        public bool ValidateLogon(string account, string password)
        {
            string hashedPassword = HashPassword(password);
 
            var memberAccount = accountRepository.GetByAccount(account);
 
            if (memberAccount != null)
            {
                if (memberAccount.Equals(hashedPassword))
                {
                    string encryptTicket = Utils.SignIn(memberAccount.ID, memberAccount.Account);
                    HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptTicket);
                    authCookie.Expires = DateTime.Now.AddDays(1);
 
                    return true;
                }
            }
 
            return false;
        }
 
        private string HashPassword(string str)
        {
            string rethash = string.Empty;
            System.Security.Cryptography.SHA1 hash = System.Security.Cryptography.SHA1.Create();
            System.Text.ASCIIEncoding encoder = new System.Text.ASCIIEncoding();
            byte[] combined = encoder.GetBytes(str);
            rethash = Convert.ToBase64String(hash.Hash);
 
            return rethash;
        }
 
    }
}

先別管上面的程式是不是可以跑,只是模擬一個情境,在這個 MemberDataViewModel 類別裡面除了定義屬性之外,我也加上了幾個方法,有登入的驗證、資料的建立與更新,接著就是使用這個 ViewModel 的 Controller 內容,

LogonController.cs

public class LogonController : Controller
{
    public ActionResult Logon()
    {
        return View();
    }
 
    [HttpPost]
    public ActionResult Logon(string account, string password)
    {
        if (!string.IsNullOrWhiteSpace(account) && !string.IsNullOrWhiteSpace(password))
        { 
            MemberDataViewModel viewModel = new MemberDataViewModel();
            bool checkLogon = viewModel.ValidateLogon(account, password);
 
            if (checkLogon)
            {
                return RedirectToAction("Index", "Home");
            }
            else
            {
                return View();
            }
        }
        return View();
    }
 
    [Authorize]
    public ActionResult MemberData()
    {
        if (Utils.CheckAuthenticated())
        {
            string encryptedTicket = Request.Cookies[FormsAuthentication.FormsCookieName].Value;
 
            Guid AccountID = Utils.GetMemberAccountID(encryptedTicket);
 
            var memberData = new MemberDataViewModel(AccountID);
 
            return View(memberData);            
        }
        return RedirectToAction("LogOff");
    }
 
    public ActionResult LogOff()
    {
        //原本號稱可以清除所有 Cookie 的方法...
        FormsAuthentication.SignOut();
 
        //清除所有的 session
        Session.RemoveAll();
 
        //建立一個同名的 Cookie 來覆蓋原本的 Cookie
        HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
        cookie1.Expires = DateTime.Now.AddYears(-1);
        Response.Cookies.Add(cookie1);
 
        //建立 ASP.NET 的 Session Cookie 同樣是為了覆蓋
        HttpCookie cookie2 = new HttpCookie("ASP.NET_SessionId", "");
        cookie2.Expires = DateTime.Now.AddYears(-1);
        Response.Cookies.Add(cookie2);
 
        //將使用者導出去
        return RedirectToAction("Index", "Home");
    }
 
}

上面就是 LogonController 的內容,登入的驗證是透過 MemberDataViewModel 內的 ValidateLogon() 方法,取得一筆從資料庫載入資料的 MemberDataViewModel 實例也是透過 MemberDataViewModel 這個類別,再來就是當登入之後 Member 要查看自己的 Member 資料則要到 MemberData 的 View,MemberData 頁面上的資料類別為「MemberDataViewModel」,

View - MemberData.cshtml

image

使用自訂類別為頁面 Model 是沒有辦法在建立 View 時套用範本的,所以我們就需要自己編輯 View 頁面,就在編輯的時候,當要從頁面 Model 裡取出適當資料時,發現了……

image

在 Razor Syntax 當中可以看到 ViewModel 裡面的方法成員(Create, Update, ValidateLogon),這樣好嗎?

 

ViewModel 加入行為有什麼問題?

在 ASP.NET MVC 剛推出的時候,很多人就對 View 裡面的 <% …… %>的使用覺得感冒,好像這樣又回到了以前 ASP 那種義大利麵式的程式寫法,這是絕大部分剛接觸或是未接觸的人才會有這樣的想法,等到實際有使用 ASP.NET MVC 來開發的時候就不會有這樣的想法,因為我們寫 ASP.NET MVC 的時候就會被一直提醒不要在 View 裡面去加入太多的程式,這就是避免義大利麵式的編程方式會在專案裡蔓延。

到了 ASP.NET MVC 3 推出後,Razor Syntax 比以往 WebForm ViewEngine 的寫法更為簡潔,而且程式與 html tag 有時是可以混在一起的,所以常常會有很多剛開始學習 ASP.NET MVC 或是已經開發一段時間的工程師就會在 View 頁面裡加入大量的程式邏輯。

在 View 頁面不能寫程式邏輯嗎?

當然可以寫,只是過多的程式邏輯就代表著難以維護,在設計階段,預設是不會對 View 進行編譯的,必須要等到我們按下 F5 執行網站或是部屬到網站後才能看到頁面是否正常執行或是發生錯誤,所以大部分的 ASP.NET MVC 開發者在頁面上的程式邏輯只會針對顯示來做處理,而不應該直接處理 Model 資料的操作。

在 MemberDataViewModel 這個類別裡,裡面的方法放在 MemberDataViewModel 適當嗎?

Controller 本身的職責就是在於處理資料的輸出入,然後決定要回應什麼樣的結果,所以我在「分層架構」這個系列的文章裡所說的就是我們可以把資料的處理給切割出來,建立適當的層來做事,切出 Repository 用來處理資料的存取,切出 Service 則是用來處理商業邏輯並服務 Controller,而當 Controller 收到有資料要建立或是更新的需求時,則收到的資料交給 Servive 並在一些處理後,最後交由 Repository 存到資料庫,當 Controller 收到要取出資料的需求,將取資料的條件交給 Service,Service 做過處理後(判斷條件值是否合適、正確等)再透過 Repository 從資料庫取得資料,而取得的資料視狀況需求,看 View 所要的 Model 類別為何,傳出 Model 或是選用適當的 ViewModel 類別,將資料 Bind 到 ViewModel 後再給 View。

前面的 MemberDataViewModel 類別內的方法其實都是做了 Service 或是 Repository 的工作,當 MemberDataViewModel  也包含了 Server 與 Repository 的行為時,MemberDataViewModel 的值則是什麼呢?

也許有人並沒有在 MVC 專案裡去切出 Service 層,所以就把商業邏輯處理給放在 ViewModel 類別中,那如果每個頁面所需要的資料都不同時,就必須要建立很多個 ViewModel 類別,那麼是不是每個 ViewModel 類別都要加入存取資料的行為呢?當 ViewModel 的需求越來越多,每個 ViewModel 的成員都不同時,每個 ViewModel 都有行為存在, Controller 與 View  就會十分依賴 ViewModel 了,這樣的做法恰當嗎?

 

修改

最後我們再把前面的那個 LogonController 與 MemberDataViewModel 做個修改,加入 Service 層,來比較看看哪一種的使用情境會比較適合。

MemberDataViewModel.cs

將 ViewModel 類別內的行為都移除,只剩下兩個屬性的資料成員,

image

 

MemberService.cs

這裡負責 MemberAccount 與 MemberDetail 的業務邏輯操作,有需要 Member 相關資料或是業務邏輯的服務就是找 MemberService 類別,

namespace ViewModelSample.Services
{
    public class MemberService
    {
        private MemberAccountRepository accountRepository;
        private MemberDetailRepository detailRepository;
 
        public MemberService()
        {
            this.accountRepository = new MemberAccountRepository();
            this.detailRepository = new MemberDetailRepository();
        }
 
        public void CreateAccount(MemberAccount account)
        {
            this.accountRepository.Create(account);
            this.detailRepository.SaveChanges();
        }
 
        public void CreateDetail(MemberDetail detail)
        {
            this.detailRepository.Create(detail);
            this.detailRepository.SaveChanges();
        }
 
        public void UpdateAccount(MemberAccount account)
        {
            this.accountRepository.Update(account);
            this.detailRepository.SaveChanges();
        }
 
        public void UpdateDetail(MemberDetail detail)
        {
            this.detailRepository.Update(detail);
            this.detailRepository.SaveChanges();
        }
 
        public MemberDataViewModel GetMemberData(Guid id)
        {
            var accountData = this.accountRepository.Get(id);
            if (accountData != null)
            {
                var detailData = this.detailRepository.GetByAccountID(accountData.ID);
 
                var memberData = new MemberDataViewModel()
                {
                    Account = accountData,
                    Detail = detailData
                };
 
                return memberData;
            }
            return null;
        }
 
        public bool ValidateLogon(string account, string password)
        {
            string hashedPassword = HashPassword(password);
 
            var memberAccount = accountRepository.GetByAccount(account);
 
            if (memberAccount != null)
            {
                if (memberAccount.Equals(hashedPassword))
                {
                    string encryptTicket = Utils.SignIn(memberAccount.ID, memberAccount.Account);
                    HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptTicket);
                    authCookie.Expires = DateTime.Now.AddDays(1);
 
                    return true;
                }
            }
 
            return false;
        }
 
        private string HashPassword(string str)
        {
            string rethash = string.Empty;
            System.Security.Cryptography.SHA1 hash = System.Security.Cryptography.SHA1.Create();
            System.Text.ASCIIEncoding encoder = new System.Text.ASCIIEncoding();
            byte[] combined = encoder.GetBytes(str);
            rethash = Convert.ToBase64String(hash.Hash);
 
            return rethash;
        }
    }
}

 

LogonController.cs

相關的資料存取與業務邏輯處理都是使用 MemberService,

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using ViewModelSample.Misc;
using ViewModelSample.Services;
 
namespace ViewModelSample.Controllers
{
    public class LogonController : Controller
    {
        private MemberService memberService = new MemberService();
 
        public ActionResult Logon()
        {
            return View();
        }
 
        [HttpPost]
        public ActionResult Logon(string account, string password)
        {
            if (!string.IsNullOrWhiteSpace(account) && !string.IsNullOrWhiteSpace(password))
            {
                bool checkLogon = memberService.ValidateLogon(account, password);
 
                if (checkLogon)
                {
                    return RedirectToAction("Index", "Home");
                }
            }
            return View();
        }
 
        [Authorize]
        public ActionResult MemberData()
        {
            if (Utils.CheckAuthenticated())
            {
                string encryptedTicket = Request.Cookies[FormsAuthentication.FormsCookieName].Value;
 
                Guid accountID = Utils.GetMemberAccountID(encryptedTicket);
 
                var memberData = memberService.GetMemberData(accountID);
 
                return View(memberData);            
            }
            return RedirectToAction("LogOff");
        }
 
        public ActionResult LogOff()
        {
            //原本號稱可以清除所有 Cookie 的方法...
            FormsAuthentication.SignOut();
 
            //清除所有的 session
            Session.RemoveAll();
 
            //建立一個同名的 Cookie 來覆蓋原本的 Cookie
            HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
            cookie1.Expires = DateTime.Now.AddYears(-1);
            Response.Cookies.Add(cookie1);
 
            //建立 ASP.NET 的 Session Cookie 同樣是為了覆蓋
            HttpCookie cookie2 = new HttpCookie("ASP.NET_SessionId", "");
            cookie2.Expires = DateTime.Now.AddYears(-1);
            Response.Cookies.Add(cookie2);
 
            //將使用者導出去
            return RedirectToAction("Index", "Home");
        }
 
    }
}

 

MemberData.cshtml

Model 除了 object 的基本方法外,只會出現自訂的屬性成員而不會有讓人迷惑的行為,編輯 View 時會更加單純。

image

 

2014-03-24 補充網站專案的目錄架構

image



最後還是建議各位對於 ASP.NET MVC - ViewModel 應該 ……

ViewModel 不該有 Repository or Service 的屬性存在。ASP.NET MVC 所建立的 ViewModel 只是單純用來給 View 呈現資料,所以不需要也不應該加入行為(方法),尤其是會直接對 Model 內容的更改(CRUD)。

 

延伸閱讀:

InfoQ - 视图模型(View-Model)到底是什么?

91 哥所提供的 View-Model 說明文章,不過這一篇的 View-Model 是以 MVVM 的角度來做說明,而內容有提到一部分有關於 MVC 的 ViewModel,不過還是以 MVVM 的角度來做說明(因為 MVC 的 ViewModel 在那篇文章中並不是重點),所以文章裡面對 MVC 的 ViewModel 就不是那麼明確與詳細。

 

以上

15 則留言:

  1. 講述得非常清楚,很受用,如果可以把說明時的程式碼在方案總管的檔案結構截圖的話,閱讀到最後比較容易了解,謝謝。

    回覆刪除
    回覆
    1. 感謝你的回應,在最後補上一張網站的專案目錄架構擷圖,與你所說在每個程式碼的說明步驟放上檔案結構擷圖的意思不同,但還是希望能夠讓其他朋友可以有所幫助。

      刪除
    2. 謝謝你快速地回覆

      刪除
  2. 最近重新複習Kevin老師上課的講義,收穫良多,非常感謝

    回覆刪除
    回覆
    1. 謝謝你的回應,多看幾次就會有更多的發現。
      每次的備課過程中,也都會發現以前沒有注意過的。

      刪除
  3. 作者已經移除這則留言。

    回覆刪除
  4. 請問老師一個問題
    假如有一個User的Table (userid , username , birthday ....)
    做了一個UserViewModel中有一個property 為 User Model 類別
    這個可以提供View在顯示時使用
    假如在新增的View也共用UserViewModel且要使用DataAnnotations,
    那在UserViewModel中再各別新增每個㯗位(userid , username , birthday ....)的property
    去使用DataAnnotations這樣做法是否恰當?

    回覆刪除
    回覆
    1. 其實 ViewModel 類別本身與原生的 Entity Model 類別都是一樣的,
      都是可以使用 DataAnnotation 的 Attribute,
      而 ViewModel 類別主要的目的就是給「View」來使用,
      所以我會建議一個 View 就建立一個專屬的 ViewModel,這麼做會比較適當,
      因為共用所衍生的問題會蠻多的,所以我並不會建議多個頁面去共用某一個 ViewModel。

      你可以建立一個使用 Basic 範本的 ASP.NET MVC 專案,並且使用 ASP.NET Identity 的驗證,
      在建立好的專案中,可以到「Models」目錄去開啟 AccountViewModel.cs 與 ManageViewModel.cs,
      觀察這兩個 ViewModel 類別的內容,並且去看看使用這兩個 ViewModel 的 Controller 與 View,
      使用情境就如同你所提問的是相似的。

      刪除
    2. 不好意思,接著請問一下老師,

      假如有很多的View頁面,還是會傾向為每一個view建立自己viewmodel嗎?
      如果是的話,那viewmodel資料夾的建立方式,是否就像view資料夾一樣?
      非常感謝

      刪除
    3. Hello, 你好
      是的,我會建議一個 View 應該建立專屬的 ViewModel,不過這要看是否真的需要這麼做,
      以會員的 Model 來說,一個會員的類別會有許多的欄位,但是在會員登入的頁面並不需要直接使用會員類別來作為頁面的 Model,
      而應該是另外建立一個 LoginViewModel 來給 View 使用,而其他頁面的做法也是一樣,
      所以我會說,原則上我建議每一個頁面應該建立專屬的 ViewModel,但是要看是否真的需要,
      不然建立一大堆 ViewModel 類別,在管理上也上很麻煩,
      至於 ViewModel 與 Model 的轉換就可以使用 AutoMapper 來處理,使用的方式在這個部落格裡也有做說明。

      放置 ViewModel 的資料夾,如果小系統,類別數量並不是很多,可以直接放在 Models 資料夾裡,
      或者是在 Models 裡再建立子目錄 ViewModels,
      又或者也可以直接在專案根目錄下建立 ViewModels 資料夾,
      其實 ViewModel 就是個單純的類別,放在什麼地方都沒有關係,
      重點在於開發團隊要有共同的默契,要能夠讓開發人員一眼就可以了解,並且易於管理,
      放在哪裡並沒有什麼強制規定或是規範,但是放置的地方也需要合理(例如 ViewModels 資料夾放在 App_Start 下就不對啦)

      以上

      刪除
  5. 作者已經移除這則留言。

    回覆刪除
  6. 請問一下 ViewModel適合 實作IValidatableObject嗎?
    例如 我想要 在我修改密碼的時候 模型驗證 新密碼跟前三次的舊密碼(存在資料庫)是否有相同
    感覺這邏輯 或是行為 挺適合放在 這個為了修改密碼建的ViewModel中
    因為在存取資料庫 所以也要呼叫Service
    方便給我建議嗎?!

    回覆刪除
    回覆
    1. 可以是可以,但是我不會這麼做。
      IValidatableObject 的 Validate 我會用來做輸入資料的驗證,
      因為有些資料驗證處理是無法單純使用 DataAnnotation Attribute 去處理,
      所以就會繼承實作 IValidatableObject 另外去處理。
      .
      我的原則還是 ViewModel 只是單純的資料容器,不做過多的行為處理(我會堅持除非必要,不然不能有行為)
      因為不做這樣的規範,在多人開發的情況下,會有很多驚喜在後續的維護裡發現。
      .
      各層有各自處理的職責,所以不去做不屬於該層的職責之外的事情,避免衍生多的問題,
      找尋問題的時候,依照各層的職責去尋找問題點,會比較清晰,
      如果有各曾有職責交叉或做了不事該層職責的事情,釐清問題點就是個麻煩。
      .
      可參考以下的文章
      http://patrickdesjardins.com/blog/enterprise-asp-net-mvc-part-6-the-three-layers-of-validation
      .
      另外可以從是否具有可測試性的角度去思考你的問題。

      刪除
  7. 無意間發現有4個Repository打成Respository :D

    回覆刪除
    回覆
    1. 這不是小學生作業,知道意思就好了
      不要刻意跟我說有錯字,我不會改,謝謝

      刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。

最近的留言