[Windows Forms] 業務邏輯程式碼產生器範例 #CH4
程式碼產生器是讓工程師快速開發專案非常好用的工具,當專案底層架構設計好之後,會發現有許多程式碼長的非常相似,語法結構一樣,只是欄位名稱不同而已,這時候非常適合使用程式碼產生器來完成一部份的工作。
例如後台單檔維護的程式,我們會針對一張資料表設計一個維護界面,每個界面的操作模式一樣,差別就是資料表與欄位不同,在過去我們會用 Copy, Paste 方式來開發每一個單檔維護程式,但如果善用程式碼產生器,速度會快上許多,這也是提昇開發速度的小秘訣。
在上一章節我們花了一些時間建置資料庫底層架構,把許多共用語法抽離至底層,建立業務邏輯類別來處理資料庫互動,但各位有沒有發現,在業務邏輯類別 BusAnnouncement.cs 裡面的語法是有規則性的,每一張資料表都擁有 4 種動作,Get, Insert, Update, Delete
,每個方法裡面也針對資料欄位動態產生 SQL 語法。
紅框圈起來的地方,就是有規則性的語法,換一個資料表時,就只要換掉其中的名稱就行了。
面對大量規則性的工作,與其一直 Copy, Paste,不如花點時間寫個程式碼產生器,不止提昇能力,還會有成就感。
接下來在這章節裡面,我將會分享如何建置程式碼產生器,來產生業務邏輯類別的部份語法。
Contents
新增專案
我們之前建的專案是 ASP.NET Core MVC,但是程式碼產生器它是一個輔助功能,可以獨立執行,也適合用在下一個專案,所以我這次會新增 Windows Form 專案,寫一個視窗程式來產生程式碼。
在方案按右鍵,加入一個「新增專案」。
選擇「Windows Forms 應用程式」。
輸入專案名稱為 “CodeGen”,架構選「.NET 6.0」,就可以建立專案了。
設計畫面
在 Form1 畫面,我簡單設計了操作界面。
輸入資料庫連線字串,執行「讀取資料表」,然後選擇資料表,就可以「產生程式碼」。
範本類型我只提供業務邏輯範本,當你學會了此範本,就可以自行設計其他的範本了。
讀取資料表語法
在設計畫面連按兩下「讀取資料表」的按鈕,就會進入寫程式的畫面,可以貼上以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/// <summary> /// 讀取資料表 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnReadTable_Click(object sender, EventArgs e) { // 資料庫連線 SqlConnection conn = new SqlConnection(); conn.ConnectionString = txtConn.Text; conn.Open(); // 讀取資料表清單 StringBuilder sql = new StringBuilder(); sql.Append("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.Tables "); sql.Append("ORDER BY TABLE_NAME"); // 執行資料庫查詢動作 SqlCommand cmd = new SqlCommand(); cmd.CommandText = sql.ToString(); cmd.Connection = conn; SqlDataAdapter adpt = new SqlDataAdapter(); adpt.SelectCommand = cmd; DataSet ds = new DataSet(); adpt.Fill(ds); // 輸出資料表下拉 if (ds.Tables[0].Rows.Count > 0) { foreach (DataRow dataRow in ds.Tables[0].Rows) { cboTable.Items.Add(dataRow["TABLE_NAME"]); } cboTable.SelectedIndex = 0; } // 關閉連線 conn.Close(); } |
這裡執行後,會找到資料庫內所有的資料表清單,然後輸出到下拉列表裡面。
INFORMATION_SCHEMA.Tables
是系統的 View,此 View 會存放所有的資料表名稱。
產生程式碼語法
在設計畫面連按兩下「產生程式碼」的按鈕,就會進入寫程式的畫面,可以貼上以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/// <summary> /// 產生程式碼 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnGenCode_Click(object sender, EventArgs e) { // 資料庫連線 SqlConnection conn = new SqlConnection(); conn.ConnectionString = txtConn.Text; conn.Open(); // 取得資料表欄位 StringBuilder sql = new StringBuilder(); sql.Append("SELECT M.COLUMN_NAME, M.IS_NULLABLE, M.DATA_TYPE, R2.CONSTRAINT_NAME "); sql.Append("FROM INFORMATION_SCHEMA.Columns M "); sql.Append("LEFT JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE R2 ON R2.TABLE_NAME = M.TABLE_NAME AND R2.COLUMN_NAME = M.COLUMN_NAME AND R2.CONSTRAINT_NAME LIKE 'PK_%' "); sql.Append("WHERE M.Table_Name = '" + cboTable.Text + "' "); sql.Append("ORDER BY ORDINAL_POSITION"); // 執行資料庫查詢動作 SqlCommand cmd = new SqlCommand(); cmd.CommandText = sql.ToString(); cmd.Connection = conn; SqlDataAdapter adpt = new SqlDataAdapter(); adpt.SelectCommand = cmd; DataSet ds = new DataSet(); adpt.Fill(ds); // 範本欄位列表 List<ColumnModel> Columns = new List<ColumnModel>(); foreach (DataRow row in ds.Tables[0].Rows) { ColumnModel col = new ColumnModel(); col.COLUMN_NAME = row["COLUMN_NAME"].ToString(); col.IS_NULLABLE = row["IS_NULLABLE"].ToString(); col.DATA_TYPE = row["DATA_TYPE"].ToString(); col.CONSTRAINT_NAME = row["CONSTRAINT_NAME"].ToString(); Columns.Add(col); } // 給予樣板資料 BusinessTemplate template = new BusinessTemplate(); template.TableName = cboTable.Text; template.Columns = Columns; // 取得樣板結果 txtResult.Text = template.TransformText(); // 關閉連線 conn.Close(); } |
執行後,會從資料庫的 View 的 INFORMATION_SCHEMA.Columns 裡面找到所有的欄位,再下一個條件為要查詢的 Table 就會列出資料表的所有欄位。
有了欄位列表,就可以帶入文字範本去產生語法了。
建立 T4 文字範本
我在專案目錄下新增目錄,取名為 “Template”,然後加入「新增項目」。
在類型中選擇「執行階段文字範本」,輸入名稱為 “BusinessTemplate.tt”。
修改範本輸出類型
目前此範本它輸出類型為專案可編譯的 .cs 程式碼,會在 BusinessTemplate.tt 底下出現 BusinessTemplate.cs 的檔案,檔案內容是空的,但是這不是我們要的。
要修改輸出類型為「執行時間文字模板」,在 BusinessTemplate.tt 按右鍵選擇「屬性」。
將「自行工具」的值修改為「TextTemplatingFilePreprocessor」。
完成後,這時候再打開 BusinessTemplate.cs 檔案內容來看,他會變成 public partial class BusinessTemplate : BusinessTemplateBase
的部份類別。
可能有些人第一次看到 partial class
這種類別宣告,他的特性就是同一個類別寫在不同的檔案,最後執行時會合併在一起,官方介紹可參考這裡。
這裡會宣告為 partial class
是因為這個類別是程式自動產生,而我們可補充寫另一個部份類別來合併,我們會將範例內用到的屬性資料寫在新的部份類別內。
業務邏輯範本
打開 BusinessTemplate.tt,這裡就是放範本的地方,因為我們要產生業務邏輯的語法,而我們在 MVC 專案的 BusAnnouncement.cs 已經有了語法,只要將 BusAnnouncement.cs 裡面的語法 Copy 到 BusinessTemplate.tt 裡面,然後針對變動的地方設為變數,也可以在範本內使用迴圈,語法可參考官方介紹。
在 BusinessTemplate.tt 貼上以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
<#@ template language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> using ProjectLibrary.Base; using ProjectLibrary.DB; using System.Data; using System.Dynamic; using System.Text; namespace TeachAnnouncement.Business { public class Bus<#=TableName#> : BusinessBase { #region 建構子 public Bus<#=TableName#>(DBManager _dbManager) : base(_dbManager) { } #endregion #region 方法 /// <summary> /// 取得<#=TableName#> /// </summary> /// <returns></returns> public DataTable Get<#=TableName#>() { StringBuilder sql = new StringBuilder(); sql.Append("SELECT M.* "); sql.Append("FROM <#=TableName#> M "); sql.Append("WHERE 1=1 "); StringBuilder sbWhere = new StringBuilder(); dynamic param = new ExpandoObject(); // 動態組合 SQL 條件 <# string KeyColumn = ""; foreach (ColumnModel columnModel in Columns) { if (string.IsNullOrEmpty(columnModel.CONSTRAINT_NAME) == false) { if (KeyColumn!=""){ KeyColumn += ","; } KeyColumn += columnModel.COLUMN_NAME; #> GenWhere(sbWhere, param, "<#=columnModel.COLUMN_NAME#>", "=", <#=columnModel.COLUMN_NAME#>); <# } } #> sql.Append(sbWhere); sql.Append("ORDER BY <#=KeyColumn#>"); // 執行查詢 DataTable dt = dbManager.GetData(sql.ToString(), param, PageNo, PageSize, ref TotalRowCount); ResetColumn(); return dt; } /// <summary> /// 新增<#=TableName#> /// </summary> /// <returns></returns> public int Insert<#=TableName#>() { StringBuilder sbColumn = new StringBuilder(); StringBuilder sbValue = new StringBuilder(); dynamic param = new ExpandoObject(); // 動態組合 SQL 欄位 <# foreach (ColumnModel columnModel in Columns) { #> GenInsert(sbColumn, sbValue, param, "<#=columnModel.COLUMN_NAME#>", <#=columnModel.COLUMN_NAME#>); <# } #> // 執行新增 int cnt = dbManager.Insert("<#=TableName#>", sbColumn.ToString(), sbValue.ToString(), param); ResetColumn(); return cnt; } /// <summary> /// 修改<#=TableName#> /// </summary> /// <returns></returns> public int Update<#=TableName#>() { StringBuilder sbColumn = new StringBuilder(); StringBuilder sbWhere = new StringBuilder(); dynamic param = new ExpandoObject(); // 動態組合 SQL 欄位 <# foreach (ColumnModel columnModel in Columns) { if (string.IsNullOrEmpty(columnModel.CONSTRAINT_NAME) == true) { #> GenUpdate(sbColumn, param, "<#=columnModel.COLUMN_NAME#>", <#=columnModel.COLUMN_NAME#>); <# } } #> // 動態組合 SQL 條件 <# foreach (ColumnModel columnModel in Columns) { if (string.IsNullOrEmpty(columnModel.CONSTRAINT_NAME) == false) { #> GenWhere(sbWhere, param, "<#=columnModel.COLUMN_NAME#>", "=", <#=columnModel.COLUMN_NAME#>); <# } } #> // 執行修改 int cnt = dbManager.Update("<#=TableName#>", sbColumn.ToString(), sbWhere.ToString(), param); ResetColumn(); return cnt; } /// <summary> /// 刪除<#=TableName#> /// </summary> /// <returns></returns> public int Delete<#=TableName#>() { StringBuilder sbWhere = new StringBuilder(); dynamic param = new ExpandoObject(); // 動態組合 SQL 條件 <# foreach (ColumnModel columnModel in Columns) { if (string.IsNullOrEmpty(columnModel.CONSTRAINT_NAME) == false) { #> GenWhere(sbWhere, param, "<#=columnModel.COLUMN_NAME#>", "=", <#=columnModel.COLUMN_NAME#>); <# } } #> // 執行修改 int cnt = dbManager.Delete("<#=TableName#>", sbWhere.ToString(), param); ResetColumn(); return cnt; } #endregion #region 欄位 <# foreach (ColumnModel columnModel in Columns) { #> public object <#=columnModel.COLUMN_NAME#> { get { return htColumn["<#=columnModel.COLUMN_NAME#>"]; } set { htColumn["<#=columnModel.COLUMN_NAME#>"] = value; } } <# } #> #endregion } } |
在語法內可以看到 <#=TableName#>
是宣告一個變數,名稱為 TableName,這個會由執行時傳入資料表名稱取代,到時候輸出時就會改變。
往下語法有一段 foreach (ColumnModel columnModel in Columns)
,這個就會跑欄位的迴圈,依照資料表內的欄位數量來重複寫入欄位名稱,省下 Copy, Paste 的動作。
新增範本部份類別
在 BusinessTemplate.tt 範本內我有用到一些變數,例如 TableName, Columns
,這時候要為 public partial class BusinessTemplate
產生另一個部份類別,來存放用到的變數。
在 Template 目錄下新增新類別,取名為 “BusinessTemplateCode.cs”,
將原本的語法,改為以下以語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace CodeGen.Template { public partial class BusinessTemplate { public string TableName { get; set; } public List<ColumnModel> Columns { get; set; } } public class ColumnModel { public string COLUMN_NAME { get; set; } public string IS_NULLABLE { get; set; } public string DATA_TYPE { get; set; } public string CONSTRAINT_NAME { get; set; } } } |
這個類別主要讓我們在 Form1 表單內讀到的資料,放到這類別來,算是給予資料,就可以產生對應的語法了。
測試結果
按 F5 執行一下專案,如果你執行時,不是這個專案,可以調整設定為啟動專案。
輸入資料庫連線字串後,選擇資料表,按「產生程式碼」,就會在下面產生業務邏輯語法了。
輸出的結果,你可以跟 MVC 專案的 BusAnnouncement.cs 比對一下內容,看看差異在那裡。
會發現主架構的程式碼都是一樣的,不一樣的地方就是為了業務需求而修改的地方,程式碼產生器目的就是先產生一份基本語法,然後再針對業務需求修改,這樣就會加快開發的速度了。
輸出的結果可以完成一部份的工作,對於要寫下一個資料表維護功能,可以先產生語法後,貼到檔案內再修改少部份邏輯,就完成了一隻程式了,這樣的程式碼產生器,你是否喜歡呢?
範例下載
相關學習文章
- [ASP.NET MVC] 前台會員註冊範例教學 #CH1 (附範例)
- [ASP.NET MVC] 免費公司形象網站範本套版教學 #CH1 (附範例)
- [ASP.NET Core MVC + Vue3] 動態列表及修改教學
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝