上回約略看過Controller與View,這回輪到Model,就能完成MVC大三元(謎之聲: 可惡! 好冷...)。由於MVC 3有不少依Model自動產生View的機制,故開發時從Model入手會較省力(註: 如果客戶對UI的客製需求高則則省不了多少工,故勿存有過於美好幢景)。

為了讓範例單純一點,決定不把資料庫扯進來,寫一個用資料夾混充資料庫的PlayerModel:

using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.IO;
 
namespace FirstMvc.Models
{
    //使用檔案當成資料來源的玩家資料物件
    //先不牽扯資料庫可以更單純一點
    public class PlayerModel
    {
        [Required]
        [Display(Name = "代號")]
        [RegularExpression("[A-Za-z0-9]{3,8}",
            ErrorMessage = "限定為3-8個英文或數字")]
        public string Name { get; set; }
        [Required]
        [Display(Name = "分數")]
        [Range(0, 65535,
            ErrorMessage = "範圍: 0 - 65535")]
        public int Score { get; set; }
 
        #region 儲存相關函數
        static string storageFolder = "c:\\temp\\players";
        static string getFilePath(string name)
        {
            return Path.Combine(storageFolder, name + ".txt");
        }
        private string FilePath
        {
            get { return getFilePath(Name); }
        }
        #endregion
 
        #region 模擬 新增/修改/刪除/查詢 功能
        //儲存
        public void Save()
        {
            File.WriteAllText(FilePath, Score.ToString());
        }
 
        //讀取
        public static PlayerModel Read(string name)
        {
            string file = getFilePath(name);
            if (File.Exists(file))
                return new PlayerModel()
                {
                    Name = name,
                    Score = int.Parse(File.ReadAllText(file))
                };
            else
                return null;
        }
 
        //刪除
        public void Delete()
        {
            if (File.Exists(FilePath)) File.Delete(FilePath);
        }
 
        //清單
        public static List<string> GetPlayerNames()
        {
            //列舉所有檔案名稱(亦即玩家名稱),傳回字串陣列
            return
                Directory.GetFiles(storageFolder, "*.txt")
                .Select(o => Path.GetFileNameWithoutExtension(o)).ToList();
        }
        #endregion
    }
}

PlayerModel其實只有兩個屬性,Name及Score,其餘程式碼則用來模擬新增、修改、刪除等動作。注意[Require] [Regular Expression(…)] [Range(…)]等System.ComponentModel.DataAnnotations命名空間的Attribute,會被當成檢核資料是否正確的依據,甚至在Client端也用會同樣的檢核邏輯以Javascript進行資料驗證。把PlayModel.cs放進Models目錄下(實務上,我們甚至會將Model類別切出一顆獨立DLL類別庫),接著,預想一下使用者的操作流程:

  1. 先連上httq://server/home/create (懶得再開一個新的Controller,所以就寄生在HomeController.cs中,只新增一個Create Action),在HTML表單輸入Name及Score
  2. 按Submit送出表單,POST回httq://server/home/create,將結果存成檔案
  3. 懶得想過場,儲存完畢直接回到httq://server/home/index以求省事

我們在HomeController裡加上兩個Action,Create()及Create(PlayerModel player),分別標上[HttpGet]及[HttpPost]兩個ActionFilterAttribute,如此可區分出用httq://server/home/create時會先連上Create(),按Submit送出時再連到Create(PlayerModel player) (因為是HTTP POST)。

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Welcome to ASP.NET MVC!";
 
            return View();
        }
 
        public ActionResult About()
        {
            return View();
        }
 
        [HttpGet] // httq://server/home/create, 新增資料用
        public ActionResult Create()
        {
            //預設會對映到/Views/Home/Create.cshtml
            return View();
        }
        [HttpPost] // httq://server/home/create 送出表單時
        public ActionResult Create(PlayerModel player)
        {
            //前方送來的資料會自動對應到Player上(很神奇吧?)
            if (ModelState.IsValid) player.Save();
            //轉到httq://server/home/index
            return RedirectToAction("Index");
        }
    }

Controller設好,接下來要產生View。在Visual Studio 2010這種上流社會華麗IDE環境中,一定有不用徒手從頭寫View的方法吧? 沒錯,在Action按下右鍵:

就有精靈現身幫你實現願望... 如下圖,在Model class裡可以選到PlayerModel(記得專案要先Build一次才會出現在清單中),接著指定Scaffold template為Create(共有Create、Details、Delete、Edit、Empty、List等選擇),按下Add,VS2010會自動生出/Views/Home/Create.cshtml來。

自動產生的Create.cshtml內容不少,還有一堆validate相關的字眼,莫非它也內含前端驗證的功能?

試試便知! (註: MVC Project是Web Application Project,更改後記得要先Build再測試,修改的程式碼才會完整更新,但Razor View Engine的部分是動態編譯的,更動.cshtml後,倒是可以儲存後就生效)

很強吧! 這個自動產生的View已具備Javascript欄位檢核功能,其檢核規則就是依我們在PlayerModel上用DataAnnotations Attribute定義出來的[Require] [Range(…)]等條件,而在Create(PlayerModel player)中,我們判斷ModelState.IsValid等同做了Server端的檢核。換句話說,只需在Model設定限制,就一口氣做到了Client Side與Server Side的資料驗證!! (註: 我又來潑冷水了,當UI的客製需求高、驗證邏輯複雜時,當然不可能點一點滑鼠就靠內建功能滿足客戶所有需求,但必須要說,ASP.NET MVC架構頗具彈性,內建功能不足的部分,多半可以靠自行開發組件將其補足,原則上只要投入一些時間與研發人力,要豬飛天都是有可能的)

實地測試,輸入有效的代號(Jeffrey)及分數(32767),按下Create鈕,就可以在c:\temp\players目錄下驗收內容為32767的Jeffrey.txt檔案,很棒吧? 等等,Create(PlayerModel model)裡連個Request[“Name”]什麼都沒看到,資料是怎麼接進來的? 要解答這個疑惑,不妨在if (ModelState.IsValid)處加上中斷點:

如上圖所示,很神奇吧! 前端送來的HTML表單欄位資料被自動轉成PlayerModel物件屬性,這就是神奇的Model Binding機制,透過這種方式,程式碼變得很簡潔吧!

又到了豬腳按摩時間,下篇再續。


Comments

# by 大力

請教黑大,文中有句「實務上,我們甚至會將Model類別切出一顆獨立DLL類別庫」?意思是說,Model 放到另外一個專案來開發,方便將來移植是嗎?

# by Jeffrey

to 大力,切成獨立專案編譯成DLL的理由蠻多的,有時是為了分工(Model與UI不同人寫),有時是為了切割完整的管理或測試單位,依我自己的經驗,切割成獨立DLL的最大好處是方便不同的專案引用,例如: 後台與前台分成兩個Web App專案,而帳務Model二者共用,切成DLL專案可以避免將Model Class程式檔要複製成兩份放在兩個專案中,衍生日後維護需同步修改兩處的困擾。

# by 大力

感謝黑大指點

# by Saint

請問這一個Method 「GetPlayerNames」是否要改成Static呢?

# by Jeffrey

to Saint, 是的,應為static,程式範例漏了,謝謝指正。

Post a comment