# [112]天方科技 ASP.net core 教育訓練 1120413(MVC vs MVVM、C#存取修飾詞、C#建構子、Dto Class、Http資料繫結來源) ## MVC vs MVVM ### MVC(Model View Controller) Model 層:模型(用於封裝業務邏輯相關的數據以及對數據的操縱) View 層:視圖(渲染圖形化界面,也就是所謂的 UI 界面) Controller 層:控制器(M 和 V 之間的連接器,主要處理業務邏輯,包括顯示數據,界面跳轉,管理頁面生命週期等) ![](https://i.imgur.com/hW29MYf.png) MVC 的優點 1. 提高程式的可維護性,MVC 將一個功能區分成許多片段,讓程式變得容易維護 2. 在 MVC 中,同一個 Model 可以被不同的 View 重用,提高了程式碼的可重用性 3. MVC 的架構容易識別,可以讓工程師快速掌握項目的結構 4. MVC 將一個功能分成了三部分,因此多個工程師可以同時開發不同部分 的程式碼 MVC 的缺點 1. View 與 Controller 綁住了,不易進行 Unit Test 2. 要花費較多的時間進行前置規劃,不適合小型,中等規模的應用程序 3. 要管理較多檔案 ### MVVM( Model View ViewModel) Model 層:Model 層代表了描述業務邏輯和數據的一系列類的集合。它也定義了數據修改和操作的業務規則。 View 層:View 代表了 UI 組件,像 CSS,JQuery,html 等。他只負責展示從 Presenter 接收到的數據。也就是把模型轉化成 UI。 View Model 層:View Model 負責暴漏方法,命令,其他屬性來操作 VIew 的狀態,組裝 model 作爲 View 動作的結果,並且觸發 view 自己的事件。 ![](https://i.imgur.com/oibnXZv.png) MVVM 的優點 1. 用 Data Binding 的方式與 View/ViewController 溝通,使 ViewModel 可以不用知道是哪個 View/ViewController 2. 商業邏輯不用與 UI 綁在一起,讓 ViewController 可以瘦身 3. ViewModel 相對容易執行單元測試 (Unit Test) MVVM 的缺點 1. 大型的應用程式中,Data Binding 會導致相當大的記憶體消耗 2. 無法簡單重用 View/ViewController,因為與 ViewModel 綁定的關係 3. 並無強硬規定 View/ViewController 跟 ViewModel 的關係 (Ex:單向 or 雙向溝通) 4. 不易除錯,不容易找出 bug 是在 Model 還是 View 參考: [**MVC vs MVVM!MVVM 是什麼?能吃嗎?(上)**](https://hackmd.io/@leoho0722/rJtBdJFJc/https%3A%2F%2Fhackmd.io%2F%40leoho0722%2Frkaj8tXH9) [**vue 設計模式之 MVC、MVP 和 MVVM 三種設計模式的異同**](https://www.readfog.com/a/1649707274211528704) [**MVC V.S. MVVM 學習筆記**](https://front-chef.coderbridge.io/2021/02/27/mvc-mvvm/) ## 建立Dto Class ### 為什麼要使用DTO 1. 將服務層與數據庫層分離 2. 隱藏客戶端不需要接收的特定屬性 3. 省略屬性以減小有效負載大小 4. 操作嵌套對像以使它們對客戶更方便 5. 避免「過度張貼」漏洞 ### 建立Dto Class時可以建立一個專屬Dto的資料夾,並將檔案名稱加上Dto,容易分辨、搜尋 ![](https://i.imgur.com/wXRoMwi.png) ![](https://i.imgur.com/8f7tyC9.png) 參考: [**Day23 DTO是甚麼**](https://ithelp.ithome.com.tw/articles/10306195) [**DTO、VO、BO、DAO、POJO 各種 Object**](https://hackmd.io/@OceanChiu/HJBvCZcQ8) ## C#屬性 `get`存取子和`set`存取子 ```csharp= public class DtoStudent { //完整Property寫法 private string? _names; public string? Name //Property { get { return _names; } set { _names = value; } //accessor } //早期沒有Property的語言,需要額外寫Get以及Set方法 public string? getName() { return _names; } public void setName(string? value) { _names = value; } //當前寫法 //private string? _gender; //編譯時自動宣告 public string? Gender { get; set; }; //省略變數名稱,只留get、set //Property可以使用修飾詞 public string? Gender { get; internal set; }; //新Property init public string? Gender { get; init; }; //只能在建構函式中使用,或使用 物件初始化運算式 } ``` 參考: [**自動實作的屬性 (C# 程式設計手冊)**](https://learn.microsoft.com/zh-tw/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties) [**使用屬性 (C# 程式設計手冊)**](https://learn.microsoft.com/zh-tw/dotnet/csharp/programming-guide/classes-and-structs/using-properties) [**init 存取子**](https://learn.microsoft.com/zh-tw/dotnet/csharp/programming-guide/classes-and-structs/using-properties#the-init-accessor) ## 存取修飾詞 `public`:類型或成員可由相同元件或其他參考它的元件中的其他程式碼存取。 類型的公用成員的存取層級是由類型本身的存取層級所控制。公開 `private`:類型或成員只能由相同 `class` 或 `struct` 中的程式碼存取。類別內部可見 `protected`:類型或成員只能由相同 `class` 中的程式碼存取,或是 `class` 衍生自該`class`中。 只要有繼承就可以使用,可跨專案 `internal`:類型或成員可由相同元件中的任何程式碼存取,但無法從另一個元件存取。 換句話說, `internal` 可以從屬於相同編譯的程式碼存取類型或成員。此專案可用的任何類別 `protected internal`:類型或成員可由宣告所在的元件中的任何程式碼存取,或從衍生自另一個元件中的程式 `class` 代碼存取。只要有繼承就可以使用,可跨專案 `private protected`:類型或成員可由衍生自 class 其包含元件內宣告的 型別存取。除了類別內部,任何繼承都可以使用 ![](https://i.imgur.com/x2A1kVP.png) 參考: [**存取修飾詞 (C# 程式設計手冊)**](https://learn.microsoft.com/zh-tw/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers) ### 最上層型別修飾詞 在`namespace`下直接宣告`Value Type`和`Reference Type`的5種型別,稱為最上層型別 最上層型別只有`public`、`internal`兩種修飾詞 ### 最上層型別預設為`internal`,Class內部預設為`public` ![](https://i.imgur.com/jW7VnuE.png) ![](https://i.imgur.com/4IdAcus.png) `internal` 小愛心圖示 ![](https://i.imgur.com/CiYzylM.png) `public`圖示 ![](https://i.imgur.com/h7CIej3.png) ### `struct`內部可以使用`public`、`internal`、`private` `private`鎖圖示 ![](https://i.imgur.com/mX8L7bA.png) ### 在`class`內的方法呼叫`struct`的變數,除了`private`外都可以使用 ![](https://i.imgur.com/xZFiOQ7.png) ### `class`內部可以使用所有修飾詞 ![](https://i.imgur.com/1ONmPN2.png) ### 創建`class D`繼承`class C`,因為是繼承的關係,除了`private`外都可以使用 ![](https://i.imgur.com/wDrTB7y.png) ### 不使用繼承只能使用`public`、`internal`、`protected internal` ![](https://i.imgur.com/vHBEYFX.png) ## 建構子 ```csharp= public class DtoStudent { private string? _names; public DtoStudent() //建構子 construtor 簡稱ctor { Gender = "abc"; } public string? Gender { get; set; }; } ``` ### 將`class`實體化時,呼叫的方法就是建構子 ```csharp= class XX { void M() { DtoStudent dtoStudent = new DtoStudent(); //只有在物件建立時會呼叫建構子 } } ``` ### `init` 物件初始化運算式 ```csharp= class XX { void M() { DtoStudent dtoStudent = new DtoStudent(){ Gender = "123"} ; //init 物件初始化運算式 //如果Property是init無法直接給值 //public string? Gender { get; init; } dtoStudent.Gender = "aaa"; //error //如果Property是set可以直接給值 //public string? Gender { get; set; } dtoStudent.Gender = "aaa"; } } ``` ### `class`實體化簡寫 ```csharp= //設定 public string? Gender { get; set; } class XX { void M() { //原先寫法需要在物件建立後賦值 DtoStudent dtoStudent = new DtoStudent(); dtoStudent.Gender = "aaa"; dtoStudent.Name = "bbb"; //↓ //後來更新成物件初始化時就可以賦值,如果將set改為init就只能在初始化時賦值 DtoStudent dtoStudent = new DtoStudent() {Gender = "aaa", Name = "bbb"}; //可省略() DtoStudent dtoStudent = new DtoStudent { Gender = "123"} ; //可省略class名稱 DtoStudent dtoStudent = new () { Gender = "123"} ; } } ``` ### 建構子多載 ```csharp= //如果沒有宣告預設建構子,在compiler時會自動產生 public DtoStudent() { Gender = "abc"; } public DtoStudent(string? gender) { Gender=gender; } public DtoStudent(string? gender, string? name) { Gender = gender; Name=name; } ``` ![](https://i.imgur.com/oevg3YG.png) ### `[]` 也是使用物件初始化設定 ```csharp= //資料驗證設定 [Range(110,120, ErrorMessage ="範圍110~120之間")] public int? std_year { get; set; } //↓ // [] 可以想成 var r = new Range(110, 120) {ErrorMessage ="範圍110~120之間")}; ``` ## 使用Dto Class ### 使用Dto前,須注意存取權限`public class DtoStudent`,權限設為`public` ```csharp= [HttpPost("year/{year}")] public IActionResult GetStudentByYear(int year, DtoStudent dtoStudent) { // string name = dtoStudent.Name!; string gender = dtoStudent.Gender!; var linq = from s in _context.s30_student where s.std_year == year && (name != null && name.Length > 0 ? s.std_name.StartsWith(name) : true) && (gender != null && gender.Length == 1 ? s.std_sex == gender : true) group s by new { s.cls_id, s.std_year } into g where g.Count() > 0 select new { cls_id = g.Key.cls_id, year = g.Key.std_year, count = g.Count(), Students = g.ToList() }; return Ok(linq); } ``` ### 使用Dto物件後,參數被包在body裡面 出現`TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body.` Swagger不允許`GET/HEAD`包含`body`,可使用Postman測試 ![](https://i.imgur.com/rkbHioU.png) ![](https://i.imgur.com/7w0aJCq.png) ### 如果沒有定義預設建構子,但是有定義帶參數的建構子,容易造成執行上的error,所以建議不去特別定義建構子 ```csharp= public class DtoStudent { //有宣告帶參數的建構子,在使用時必須給予參數才能執行 public DtoStudent(string? gender) { Gender=gender; } public DtoStudent(string? gender, string? name) { Gender = gender; Name=name; } //↓ class XX { void M() { DtoStudent dtoStudent = new DtoStudent(); //出現error,因為沒有預設建構子 DtoStudent dtoStudent = new DtoStudent("M") { Name = "bbb"}; //需先給參數才能確定使用哪一個建構子 } } } ``` ### 只有定義帶參數建構子的情況下,使用Postman測試時出現反序列化失敗問題 `[ApiController]`含有序列化與反序列化功能 `Serializer`:將物件轉成Stream Data `Deserialize`:Stream Data轉成物件 ![](https://i.imgur.com/2Q2ucC9.png) ### 恢復預設建構子,或是不定義任何建構子之後,使用Postman測試 ![](https://i.imgur.com/O6p7qPk.png) ## 資料繫結 **[FromQuery]** - 從查詢字串取得值。 **[FromRoute]** - 從路由資料取得值。 **[FromForm]** - 從張貼的表單欄位取得值。 **[FromBody]** - 從要求本文取得值。 **[FromHeader]** - 從 HTTP 標頭取得值。 `HttpGet` Action,預設來源使用`[FromQuery]` ```csharp= [HttpGet("year/{year}")] public IActionResult GetStudentByYear(int year, [FromQuery]DtoStudent dtoStudent) ``` ### 使用`[FromQuery]`,只用`body`一樣可以帶出資料 ![](https://i.imgur.com/reD63hL.png) ### `Query String`和`body`同時存在也可以 ![](https://i.imgur.com/xPq0otT.png) ### 預設來源設定為`[FromQuery]`後,修改`Query String`與`body`不一致 可以看出資料繫結時會先掃描`Query String`,如果有得到參數,就停止掃描 ![](https://i.imgur.com/LsMK0UY.png) ### `Query String`與`body`不一致的狀況下,預設來源設定為`[FromBody]`後,改為先掃描`Request Body` ![](https://i.imgur.com/MSEHsF6.png) ### 使用`form-data`傳值 ![](https://i.imgur.com/kBaIAxo.png) ![](https://i.imgur.com/PbNjEhM.png) ### 使用`x-www-form-urlencoded`傳值 將`Query String`搬入`body`裡 ![](https://i.imgur.com/0loHbwb.png) ![](https://i.imgur.com/viKGtOM.png)