# MVC架構教學 ## 定義 **<font color="#000000">什麼是MVC?</font>** > MVC模式(Model–view–controller)是軟體工程中的一種軟體架構模式,把軟體系統分為三個基本部分: > **<font color="#f00">模型(Model)</font>**、**<font color="#f00">視圖(View)</font>**、**<font color="#f00">控制器(Controller)</font>** > >MVC模式的目的是實現一種動態的程式設計,使後續對程式的修改和擴充簡化,並且使程式某一部分的重複利用成為可能。除此之外,此模式透過對複雜度的簡化,使程式結構更加直覺。軟體系統透過對自身基本部分分離的同時也賦予了各個基本部分應有的功能。專業人員可以依據自身的專長分組。 **<font color="#000000">MVC架構的意義?</font>** >而對Model、View、Controller 的區分,是希望能把應用程式的內部運作歸納成不同的部門,讓每個部門各自負責不同的關注點。具體的行為是 ++「把不同意義的程式碼放在不同的檔案裡」++ > ![image](https://hackmd.io/_uploads/HJB7rX6ZR.png) * 模型(Model) - 程式設計師編寫程式應有的功能(實現演算法等等)、資料庫專家進行資料管理和資料庫設計(可以實現具體的功能)。 * 視圖(View) - 介面設計人員進行圖形介面設計。 * 控制器(Controller)- 負責轉發請求,對請求進行處理。 ![image](https://hackmd.io/_uploads/BJ6dqQpZR.png) ## Model Model 主要管理與資料邏輯有關的事項,負責邏輯管理與存儲資料數據的容器,和資料庫溝通。這裡我們要先注意:應用程式和資料庫是兩個不同的東西,在應用程式裡想要做「新增/瀏覽/修改/刪除」的動作,就需要先有 Model 層幫忙去資料庫裡取出必要的資料,把資料放進應用程式裡的某個程式物件。 Model 層被稱做 **<font color="#2894FF">邏輯層</font>** ,更明確一點說,是和「商業邏輯」有關的功能,例如: 電商網站裡會員購物有九折、訂單超過一定的金額免運費,這些邏輯出自於產品本身的需求或是規則。它們是獨立於網頁介面的商業邏輯。和這些邏輯相關的程式碼應該被分類到對應的 Model 檔案裡。 --- ## View View 所管理的功能層叫作 **<font color="#2894FF">表現層</font>** (presentation layer),顧名思義是負責管理畫面的呈現,也就是 HTML 樣板 (template)。 在開發框架裡,因為 HTML template 會有需要以動態顯示資料的情況 (也就是由 Model 取出的資料內容),所以 View 會再進一步運用樣板引擎 (template engine) 將資料帶入 template。 --- ## Controller Controller 掌握使用者互動邏輯,也是應用程式收發 **<font color="#f00">request/response</font>** 的核心。來自路由的 request 會先被送到 Controller,再由 Controller 通知 Model 調度資料,並且把資料傳遞給 View 來產生樣板 (template),並將呈現資料的 HTML 頁面回傳給客戶端。 你可以把 Controller 想做是 MVC 架構的中間人,它決定了應用程式的工作流程 (workflow),並且蒐集不同元件的工作結果,統一回傳給使用者。以下常見的設計問題 在 Controller 上會設置很多不同的「動作 (action)」,有點類似電視遙控器上的按鈕,只要觸發了不同的 action,Controller 就會啟動後續一系列的行為。 --- 以網路購物平台為例,Controller就像消費者在購物完畢後所要點選的送出按 鈕,而送出的動作(action)被觸發後就會往後端執行Model的邏輯程式,可能是訂單滿1000免運等一系列的邏輯判斷,處理完畢後再將資料送至Controller,最後交由View層顯示於畫面。 ## MVC 優缺點 | 優點 | Column 2 | | -------- | -------- | |使程式結構更直覺 |小型專案使用MVC開發效率低落 | |易擴充架構,使網頁開發具彈性 |整體系統架構肥大 | |維護方便、耦合性低,適合大型專案開發 |事前須嚴謹的規劃系統架構 | |程式重複利用性提高 |程式量與複雜性增加,開發成本提高 | # MVC實作範例 安裝Visual Studio 2022,並在VS Installer中安裝ASP.NET開發 ![image](https://hackmd.io/_uploads/rJUbcQ0-R.png) 建立新專案,ASP.NET Web應用程式(.NET Framework) ![image](https://hackmd.io/_uploads/HkgOsXRbA.png) 選擇MVC ![image](https://hackmd.io/_uploads/r12ra7AZC.png) 建立專案後按F5可執行顯示範例頁面 ![image](https://hackmd.io/_uploads/rJo1FYkGC.png) 或在View -> Home -> Index.html中點選滑鼠右鍵,在瀏覽器中顯示也可跳轉相同頁面 ![image](https://hackmd.io/_uploads/r1eecn31zC.png) ## 修改頁面顯示 - View 1.觀察首頁畫面大致可以分成三個部分: * 上方- 導覽列 * 中間- 幾個整齊的版面區塊 * 下方- 一行小小的文字 這些對應Index.html程式碼可以發現對應的是中間的區塊,並由許多div組成 ![image](https://hackmd.io/_uploads/r1UCeakM0.png) 2.可嘗試修改一些部份,例如: * 將ViewBag.Title改成「Jason的ASP.NET網頁」 * 將p內文改成「跟著我一起製作一個MVC架構的網站」 * 將h2標題的下方內文p改成「點我看更多」 ## 範例實作 - 會員註冊 --- ### 前置作業 - 安裝 SQL Server 2022 #### 下載&安裝 [官網下載](https://go.microsoft.com/fwlink/p/?linkid=2215158&clcid=0x404&culture=zh-tw&country=tw) or 打開 ![圖片5](https://hackmd.io/_uploads/HyLqpTxMC.png =250x) #### 安裝類型 安裝類型選擇「自訂」 ![圖片4](https://hackmd.io/_uploads/Skqs6agMC.png =450x) #### 新增 SQL Server 獨立安裝 點選左邊「安裝」,再點「新增 SQL Server 獨立安裝或將功能加入至現有安裝」。 ![圖片6](https://hackmd.io/_uploads/BJBECTxzR.png =450x) (取消勾選延伸模組,若無則略過) ![image](https://hackmd.io/_uploads/S1W_Caxz0.png =450x) #### 勾選安裝功能-資料庫引擎服務 ![圖片8](https://hackmd.io/_uploads/HkB00pgz0.png =450x) 點選「具名執行個體」,預設命名使用 “MSSQLSERVER” ,可自行更改 ![圖片9](https://hackmd.io/_uploads/Hyffk0gGA.png =450x) #### 啟用 SQL Server Agent 排程服務 SQL Server Agent 服務預設不會啟用,啟用是為維護排程設定,例如定期備份資料庫。 ![圖片10](https://hackmd.io/_uploads/H1vjyAeG0.png =450x) #### 啟用混合登入模式 選擇「混合模式」,管理者帳號為 “sa” ,密碼自行設定。 設定好密碼後按下「加入目前使用者」,最後按下一步。 ![圖片11](https://hackmd.io/_uploads/ByO2x0xfC.png =450x) ### 啟用 TCP/IP 連接與防火牆設定 若我們的資料庫是獨立主機,而應用程式與開發環境來自外部電腦,那就需要針對此伺服器開啟 TCP/IP 連接與防火牆設定,這樣才能由外部電腦連線到此 SQL Server。 #### 啟用 TCP/IP 連接 1. 開啟「SQL Server 2022 設定管理員」 ![圖片13](https://hackmd.io/_uploads/HycNGRefR.png =300x) 2. 左邊選項開啟「SQL Server 網路組態 > MSSQLSERVER_2019 的通訊協定」(剛剛在安裝SQL Server時「具名執行個體」取名的),可以看到右側「TCP/IP」是停用的狀態。 ![圖片14](https://hackmd.io/_uploads/HkV7Q0gfA.png =450x) 3. 雙擊「TCP/IP」,出現此畫面 ![圖片15](https://hackmd.io/_uploads/S1YhmAlzR.png =300x) 將「已啟用」選項改為「是」 4. 點擊上方「IP 位址」頁籤,在下方「IPALL」的「TCP 通訊埠」右側輸入“1433” ![圖片16](https://hackmd.io/_uploads/BJpZr0ezR.png =300x) #### 啟用 1433 防火牆 由於Windows Server 預設是關閉多數連接埠的,因此需要開啟剛剛設定的1433 Port。 1. 開啟控制台「Windows Defender 防火牆」功能 ![圖片17](https://hackmd.io/_uploads/BJC_HRezC.png =300x) 2. 開啟「進階設定」 ![圖片18](https://hackmd.io/_uploads/BySbLRxM0.png =200x) 3. 點選左測「輸入規則」,再點選右邊「新增規則」 ![圖片19](https://hackmd.io/_uploads/BJhNt0ezA.png) 4. 選擇「連接埠」 ![圖片20](https://hackmd.io/_uploads/SJCUtRezR.png =450x) 5. 在「特定本機連接埠」輸入 “1433” ![圖片21](https://hackmd.io/_uploads/HyVCFClMR.png =450x) 6. 輸入此開放防火牆的名稱(可隨意命名) ![圖片22](https://hackmd.io/_uploads/HyY85CgzA.png =450x) ### 下載 SQL Server Management Studio (SSMS) 資料庫管理介面 1. 開啟 SQL Server Management Studio (SSMS) 或 [下載](https://aka.ms/ssmsfullsetup) ![圖片24](https://hackmd.io/_uploads/Hku5oAefC.png =250x) 2. 登入 SQL Server ![圖片25](https://hackmd.io/_uploads/HyPu2AlGC.png =450x) 因為剛剛已經啟用了 TCP/IP 及混合登入模式,所以我們登入就可以輸入 127.0.0.1 或主機名稱、 sa 帳密登入。 * 注意「加密」欄位要改成「選擇性」 --- ### 實作 - 調整 MVC 範本佈局頁 https://github.com/eermagic/aspnet-mvc-member #### 引用 Vue.js 底層元件 1. 打開 \Views\Shared\_Layout.cshtml 頁面。 ![圖片1](https://hackmd.io/_uploads/HkG3AogGC.png =200x) 2. 在第37行 @Scripts.Render("~/bundles/bootstrap") 語法下加入: `<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>` ![圖片2](https://hackmd.io/_uploads/H1iie2eGC.png) #### 增加註冊選單 同樣在 _Layout.cshtml 頁面原有的選單後增加新連結: `<li>@Html.ActionLink("註冊", "Register", "Member")</li>` ![圖片3](https://hackmd.io/_uploads/Hkh4f3gMA.png) #### 增加新 Controller – MemberController 1. 將所有處理會員的頁面及邏輯都放在 MemberController 裡 ![圖片26](https://hackmd.io/_uploads/SkLZ1k-fA.png) 2. 加入「MVC 5 控制器-空白」,取名為「MemberController」 ![圖片27](https://hackmd.io/_uploads/rkbFJyZz0.png) #### 增加註冊頁面 Action 在 MemberController 頁面新增一個回傳 Register 的 View 頁面: ``` // GET: 註冊頁面 public ActionResult Register() { return View(); } ``` #### 增加註冊頁面 View 1. 在 Register() 語法上按右鍵選「新增檢視」 ![圖片28](https://hackmd.io/_uploads/ByswxkZGA.png) 2. 選擇「MVC 5 檢視」加入,確認名稱為 “Register”,有勾選「使用版面配置頁」 ![圖片29](https://hackmd.io/_uploads/Skp1byWz0.png) 3. 新增之後在 Views\Member\Register.cshtml 會新增 View 檢視頁面 ![圖片30](https://hackmd.io/_uploads/Hkvu-y-M0.png) #### 編寫註冊 View 語法 因為 VS 在 MVC 的專案,預設加入了 jQuery 及 Bootstrap 的元件,因此可以直接使用 Bootstrap 語法來設計畫面。 在 Views\Member\Register.cshtml 增加以下語法: ``` <div class="panel panel-primary"> <div class="panel-heading">註冊頁面範例</div> <div class="panel-body"> <div class="form-group"> <label>帳號</label> <input type="text" class="form-control"> </div> <div class="form-group"> <label>密碼</label> <input type="password" class="form-control"> </div> <div class="form-group"> <label>姓名</label> <input type="text" class="form-control"> </div> <div class="form-group"> <label>EMail</label> <input type="text" class="form-control"> </div> </div> <div class="panel-footer"> <button type="button" class="btn btn-primary">註冊</button> </div> </div> ``` 接著按 F5 執行專案,切換到「註冊」頁,畫面就會出現輸入表單 ![圖片31](https://hackmd.io/_uploads/Sy9xGkWM0.png =400x) #### 加入 Vue.js 控制元件 前面 Layout.cshtml 已經加了 Vue.js 的底層元件,因此這註冊頁面,就可以套用 Vue.js 的寫法。 將以下程式碼加在 Views\Member\Register.cshtml 之後: ``` //使用 Bootstrap Modal 樣式,當執行有錯誤時,顯示錯誤訊息 <div class="modal fade" id="ErrorAlert" tabindex="-1" role="dialog"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <h4 class="modal-title">錯誤訊息</h4> </div> <div class="modal-body" id="ErrorMsg" style="overflow-x:auto;width:100%;"> </div> </div> </div> </div> </div> @section scripts { <script> var VuePage = new Vue({ el: '#VuePage' , data: function () { var data = { form: {} }; return data; } , methods: { // 執行註冊按鈕 DoRegister: function () { var self = this; // 組合表單資料 var postData = {}; postData['UserID'] = self.form.UserID; postData['UserPwd'] = self.form.UserPwd; postData['UserName'] = self.form.UserName; postData['UserEmail'] = self.form.UserEmail; // 使用 jQuery Ajax 傳送至後端 $.ajax({ url:'@Url.Content("~/Member/DoRegister")', method:'POST', dataType:'json', data: { inModel: postData }, success: function (datas) { if (datas.ErrMsg) { alert(datas.ErrMsg); return; } alert(datas.ResultMsg); }, error: function (err) { $('#ErrorMsg').html(err.responseText); $('#ErrorAlert').modal('toggle'); }, }); } } }) </script> ``` #### 編寫註冊 Controller 語法 剛剛在 View 建立了一個動作呼叫 Member/DoRegister ,因此需要在 MemberController 建立回應的方法。 開啟 \Controllers\MemberController.cs,並在先前撰寫的Register() 功能後新增以下程式碼: ``` /// 執行註冊 public ActionResult DoRegister(DoRegisterIn inModel) { DoRegisterOut outModel = new DoRegisterOut(); if (string.IsNullOrEmpty(inModel.UserID) || string.IsNullOrEmpty(inModel.UserPwd) || string.IsNullOrEmpty(inModel.UserName) || string.IsNullOrEmpty(inModel.UserEmail)) { outModel.ErrMsg = "請輸入資料"; } else { SqlConnection conn = null; try { // 資料庫連線 string connStr = System.Web.Configuration.WebConfigurationManager.ConnectionStrings["ConnDB"].ConnectionString; conn = new SqlConnection(); conn.ConnectionString = connStr; conn.Open(); // 檢查帳號是否存在 string sql = "select * from Member where UserID = @UserID"; SqlCommand cmd = new SqlCommand(); cmd.CommandText = sql; cmd.Connection = conn; // 使用參數化填值 cmd.Parameters.AddWithValue("@UserID", inModel.UserID); // 執行資料庫查詢動作 DbDataAdapter adpt = new SqlDataAdapter(); adpt.SelectCommand = cmd; DataSet ds = new DataSet(); adpt.Fill(ds); if (ds.Tables[0].Rows.Count > 0) { outModel.ErrMsg = "此登入帳號已存在"; } else { // 將密碼使用 SHA256 雜湊運算(不可逆) string salt = inModel.UserID.Substring(0, 1).ToLower(); //使用帳號前一碼當作密碼鹽 SHA256 sha256 = SHA256.Create(); byte[] bytes = Encoding.UTF8.GetBytes(salt + inModel.UserPwd); //將密碼鹽及原密碼組合 byte[] hash = sha256.ComputeHash(bytes); StringBuilder result = new StringBuilder(); for (int i = 0; i < hash.Length; i++) { result.Append(hash[i].ToString("X2")); } string NewPwd = result.ToString(); // 雜湊運算後密碼 // 註冊資料新增至資料庫 sql = @"INSERT INTO Member (UserID,UserPwd,UserName,UserEmail) VALUES (@UserID, @UserPwd, @UserName, @UserEmail)"; cmd = new SqlCommand(); cmd.Connection = conn; cmd.CommandText = sql; // 使用參數化填值 cmd.Parameters.AddWithValue("@UserID", inModel.UserID); cmd.Parameters.AddWithValue("@UserPwd", NewPwd); // 雜湊運算後密碼 cmd.Parameters.AddWithValue("@UserName", inModel.UserName); cmd.Parameters.AddWithValue("@UserEmail", inModel.UserEmail); // 執行資料庫更新動作 cmd.ExecuteNonQuery(); outModel.ResultMsg = "註冊完成"; } } catch (Exception ex) { throw ex; } finally { if (conn != null) { //關閉資料庫連線 conn.Close(); conn.Dispose(); } } } // 輸出json ContentResult resultJson = new ContentResult(); resultJson.ContentType = "application/json"; resultJson.Content = JsonConvert.SerializeObject(outModel); ; return resultJson; } ``` #### 設定資料庫連線字串 開啟專案根目錄下的「Web.config」,設定資料庫連線字串 在 <configuration></configuration> 範圍內加入語法: ``` <connectionStrings> <add name="ConnDB" connectionString="Data Source=127.0.0.1;Initial Catalog=Teach;Persist Security Info=false;User ID=test;Password=test;" providerName="System.Data.SqlClient"/> </connectionStrings> ``` 連線字串的參數需改成自己的環境,參數為: Data Source = 資料庫主機名稱或位址 Initial Catalog = 資料庫名稱 User ID = 帳號(預設為sa) Password = 密碼(剛剛自行設定之密碼) #### 增加註冊 Model 剛剛在 MemberController 宣告的 DoRegisterIn 及 DoRegisterOut 都是新物件名稱,所以還需要在 Model 建立類別: 1. 在「Models」按右鍵選「加入 > 類別」 ![圖片33](https://hackmd.io/_uploads/r1ahH1WM0.png) 2. 選擇「類別」,輸入與 Controller 同名的 “MemberModel”,執行「新增」加入類別 ![圖片34](https://hackmd.io/_uploads/Sy7m8kbMR.png) 3. 在 MemberModel 類別內加入 DoRegisterIn 及 DoRegisterOut 兩個新類別 ``` /// 註冊參數 public class DoRegisterIn { public string UserID { get; set; } public string UserPwd { get; set; } public string UserName { get; set; } public string UserEmail { get; set; } } /// 註冊回傳 public class DoRegisterOut { public string ErrMsg { get; set; } public string ResultMsg { get; set; } } ``` --- ### 新增資料庫與資料表 1. 開啟前置作業連線好的 SQL Server,在伺服器下的「資料庫」按右鍵新增資料庫 ![圖片35](https://hackmd.io/_uploads/Sk-1u1bz0.png =300x) 2. 命名資料庫(需與剛才建立連線之參數名稱相同) ![圖片36](https://hackmd.io/_uploads/BkHI_JZGA.png =500x) 3. 建好資料庫後,在右側新建的資料庫名稱上點選右鍵,選擇「新增查詢」 ![圖片37](https://hackmd.io/_uploads/BJJ2OJZz0.png =300x) 4. 使用語法新增資料表 ``` CREATE TABLE [dbo].[Member]( [UserID] [varchar](10) NOT NULL, [UserPwd] [varchar](64) NOT NULL, [UserName] [nvarchar](20) NOT NULL, [UserEmail] [varchar](50) NOT NULL, CONSTRAINT [PK_Member] PRIMARY KEY CLUSTERED ( [UserID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] ``` ### 測試範例 完成程式碼及資料庫後,就可以執行「F5」運行專案。 1. 切換至「註冊」範例,輸入資料後,執行「註冊」功能。 2. 確認畫面出現「註冊完成」後,就可以檢查資料庫。 ![圖片38](https://hackmd.io/_uploads/Hy8cckWz0.png) 4. 使用 SSMS 登入資料庫,執行查詢語法檢查 Member 資料 ``` SELECT TOP (1000) [UserID] ,[UserPwd] ,[UserName] ,[UserEmail] FROM [Teach].[dbo].[Member] ``` 結果:![圖片39](https://hackmd.io/_uploads/rJxgoybzA.png)