架構設計好簡單系列(3) - 設計自己簡單的 ORM 平台

在架構設計中,如果能夠透過範本精靈的方式,如同微軟的 Entity Framework 一樣,提供一個 UI 介面,快速的將資料庫拉出來為 實體 (Entity) ,也可以提供做為 MVC 的 ViewModel,且由 IDE 工具自動產生 Generator 好需要存取後端資料庫的程式碼,這樣豈不是更完美

上一篇『架構設計好簡單系列(2)-上雲端』文章,我們將架構好的網站發佈至雲端去執行,而在最先一開始,也就是第一篇『架構設計好簡單系列(1) - 簡單分層你的網站 (如何快速從Web Form 變成 ASP.NET MVC)』之中,我們使用了一個簡單的以 SqlBizNorthwind 與 BizNorthwind 專案,並以 Reposiroty 的樣式透過 Service 將實做的部分切割出來,只要外部 IRepository 介面不變的情況下,呼叫端不需要知道後端使用何種資料庫。

 

精簡版的 ORM 產生器


而在架構設計中,如果能夠透過範本精靈的方式,如同微軟的 Entity Framework 一樣,提供一個 UI 介面,快速的將資料庫拉出來為 實體 (Entity) ,也可以提供做為 MVC 的 ViewModel,且由 IDE 工具自動產生 Generator 好需要存取後端資料庫的程式碼,這樣豈不是更完美。

就在這個構思下,筆者約在兩個月前就開始這個 Visual Studio 範本精靈的設計工作。

 

先開發好基礎的 DAL 框架


在這個專案中,筆者使用了自行開發的 Wistron 的 DAL 框架,以這個框架為基礎,設計一個範本精靈,自動將資料表拉到專案中成為 Entity 物件,並也自動將存取資料庫的實作部分以選擇的預設 Entity 產生實作的部分。

為了實作範本精靈這一塊,先前在微軟 MVP Open Day 的時候,筆者也曾詢問過蹂躪大,與他互相討論過這個問題,他建議可參考 MSDN 的這一篇說明 How to: Use Wizards with Project Templates,其中對於如何實作 Microsoft.VisualStudio.TemplateWizard 的 IWizard 介面,說明中也提供了完整的範例,因此降低了自行實作的門檻。

根據 MSDN 文件中的說明,我們有六個方法可以在 IDE 工具在建立範本的時機點時使用。如下說明:

image

圖片取自 MSDN http://msdn.microsoft.com/en-us/library/vstudio/microsoft.visualstudio.templatewizard.iwizard(v=vs.100).aspx

 

在 MSDN 提供的範例如下程式碼:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using Microsoft.VisualStudio.TemplateWizard;
   4:  using System.Windows.Forms;
   5:  using EnvDTE;
   6:  
   7:  namespace CustomWizard
   8:  {
   9:      public class IWizardImplementation:IWizard
  10:      {
  11:          private UserInputForm inputForm;
  12:          private string customMessage;
  13:  
  14:          // This method is called before opening any item that 
  15:          // has the OpenInEditor attribute.
  16:          public void BeforeOpeningFile(ProjectItem projectItem)
  17:          {
  18:          }
  19:  
  20:          public void ProjectFinishedGenerating(Project project)
  21:          {
  22:          }
  23:          
  24:          // This method is only called for item templates,
  25:          // not for project templates.
  26:          public void ProjectItemFinishedGenerating(ProjectItem 
  27:              projectItem)
  28:          {
  29:          }
  30:  
  31:          // This method is called after the project is created.
  32:          public void RunFinished()
  33:          {
  34:          }
  35:  
  36:          public void RunStarted(object automationObject,
  37:              Dictionary<string, string> replacementsDictionary,
  38:              WizardRunKind runKind, object[] customParams)
  39:          {
  40:              try
  41:              {
  42:                  // Display a form to the user. The form collects 
  43:                  // input for the custom message.
  44:                  inputForm = new UserInputForm();
  45:                  inputForm.ShowDialog();
  46:  
  47:                  customMessage = inputForm.get_CustomMessage();
  48:  
  49:                  // Add custom parameters.
  50:                  replacementsDictionary.Add("$custommessage$", 
  51:                      customMessage);
  52:              }
  53:              catch (Exception ex)
  54:              {
  55:                  MessageBox.Show(ex.ToString());
  56:              }
  57:          }
  58:  
  59:          // This method is only called for item templates,
  60:          // not for project templates.
  61:          public bool ShouldAddProjectItem(string filePath)
  62:          {
  63:              return true;
  64:          }        
  65:      }
  66:  }

 

在 MSDN 範例與說明中,我們可以很輕易的看出來我們會需要時做的是 RunStarted 、ProjectFinishedGenerating、RunFinished 等方法,其中最主要的是 RunStarted 方法,它是在當你在 Visual Studio 確認點選某個範本的時候發生,也是一開始最先引發的事件。而 ProjectFinishedGenerating 是當整個專案正在進行 Generator 時所引發的事件,然後所有作業都完成時會呼叫 RunFinished 事件。所以如果有什麼需要釋放的或是清空的變數可以在 RunFinished 事件引發的時候進行清空。

 

設計自己的範本精靈

在了解這些原理之後,各位有沒有覺得躍躍欲試呢?那就跟著筆者順著以下的步驟來做一遍吧!不過本篇文章只會描述概念,礙於公司,所以可能不會提供程式碼的下載,敬請見諒。


注意:

本實做目前使用 Visual Studio 2010,未來會修改為支援 Visual Studio 2012 的 MVC 4.0 的範本專案。


步驟一

建立類別庫專案,並加入 EnvDTE 的參考

image

它的實體檔案為 Microsoft.VisualStudio.TemplateWizardInterface.dll ,您可以在這個路徑中找到:

c:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.TemplateWizardInterface.dll

 

步驟二

(1). 加入實作 IWizard 介面的類別,也可複製 MSDN 上的範例來直接修改。

(2). 在專案中加入簽署,因為此 DLL 必須註冊到 GAC 中才可以被 Visual Studio 叫用

image

 

步驟三

建立一個空的 ASP.NET MVC 4 Web Application 當作預設樣版。

在這邊我們還有些小步驟要分批進行:

(1). 匯出範本檔案

image

(2). 修改匯出的範本中,Visual Studio 自動幫您產生的 MyTemplate.vstemplate 檔案

image

如上,在最下方,在 TemplateContent 的下方加入 WizardExtension 敘述,注意要被 VSTemplate 包起來。

看到這裡就明顯的知道 Visual Studio 是如何叫用外部的 Extension Assembly 的。

 

(3). 規畫範本檔結構

image

這裡決定 Service 的擺放位置與到時自動產生 Entity 的擺放位置,不過當然,在 IWizard 裡的 RunStarted 是允許我們動態建立資料夾與檔案的。這部分稍後說明。且在 RunStarted 中的第二個引數 Dictionary<string, string> replacementsDictionary 是會將專案中,所有文字檔案在 Generator 的時候 IDE 工具均會自動載入到 Dictionary<string, string> 變數中,所以允許我們在裡面使用 Replace 方式變更檔案內容。RunStarted 的方法宣告如下:

   1:  public void RunStarted(object automationObject,
   2:      Dictionary<string, string> replacementsDictionary,
   3:      WizardRunKind runKind, object[] customParams)
   4:  {
   5:  }

 

(4). 規畫哪一些程式檔透過 Replace 方式處理,以及在 RunStarted 內可以做的事情有哪一些

在這邊我們要規劃的項目是,比如有哪些程式檔是可以預先寫好的,只是方法帶入的參數不同,或是泛型方法帶入的 Entity Model 名稱不同而已,如果是這樣,就預先寫好放入範本專案中在匯出範本。

可以預先寫好的如:

A. DAL 的 Service 實作 BizNorthwind.cs

可以快速 Replace 的如:Namespace、泛型參數傳回值 IQueryable、方法傳入參數 等等,這些都是會隨著範本精靈動態選擇產生的。

需要 Replace 的部分使用 $變數名稱$ 刮起來,如下圖中紅圈所示:

image

其中  $safeprojectname$ 是系統所定義的,系統會自動代換為專案名稱,也就是 Assembly Name,我們不必自行 Replace。


B. 主 ASP.NET MVC 應用程式的 Controller

在 Controller 的部分大部分與上相同,只增加了 Controller 名稱,這也必須是動態由外面來決定的,唯一比較麻煩的是當要 GetOne 的時候,我必須知道您在範本精靈中選擇的 Key 名稱是哪一個,以及 Key 的型態是什麼?,以便在動態產生 Controller 的時候可以透過 Dictionary<string, string> 變數中 Replace 掉。眼尖的讀者應該會看到還有一個 $KeyType$ 變數。

需 Replace 如下紅圈所示:

image

 


註:

在會出現蚯蚓符號是當然的,這些檔案我必須使用 Zip 壓縮程式重新壓回 WistronITsMvc4Application.zip 中,再將壓好的 zip 檔案放置到 『C:\Users\[YourUserName]\Documents\Visual Studio 2010\Templates\ProjectTemplates\』 下面,加上前面加入到 MyTemplate.vstemplate 中的設定,點選建立新專案時只有這個專案會叫出範本精靈。


C. 主要連線字串

為了使自動 Generator 的程式可以直接執行,我們也將連線自窗所產生的資料庫連線相關資訊紀錄在 web.config 中。

image

D. 在 RunStarted 事件中撰寫程式碼以 Replace 上面我們定義好的 $XXXX$ 變數名稱

從剛剛到現在,我們定義了如下變數:

 

$custommessage$
$Key$
$KeyType$
$DataSource$
$InitialCatalog$
$UserID$
$Password$

 

共七個

所以筆者的作法是,先產生範本精靈視窗,當使用者點選範本精靈視窗的 OK 按鈕時,進行相關變數的 Replace 動作,做法大致是如下:

image

 

(5). 建立資料來源

這部分的規劃與筆者先前的 [MVC 開發系列] 系統分析與設計實務 [設計一個員工請假資訊系統](1) 的文章中的資料庫連線視窗的設計方式完原相同,只是照抄過來,筆者就不在這熬述。

這邊的畫面規劃如下:

image

(6). 規劃 Entity 的輸出位置

這部分的處理須放在 RunStarted 處理完成後的 ProjectFinishedGenerating 事件中執行,在前面建立好資料來源後,我們只要在 ProjectFinishedGenerating 事件裡面撰寫如下程式碼:

   1:      ProjectItem folder = null;
   2:    ProjectItem viewFolder = null;
   3:  
   4:    var result = from item in project.ProjectItems.OfType<ProjectItem>().AsEnumerable()
   5:                 where item.Name=="Models"
   6:                 select item;
   7:    
   8:    if (result.FirstOrDefault() == null)
   9:    {
  10:        folder = project.ProjectItems.AddFolder("Models");
  11:    }
  12:    else
  13:    {
  14:        //若這個目錄已經存在,則直接取得這個目錄.
  15:        folder = result.FirstOrDefault();
  16:    }
  17:  
  18:    viewFolder = folder.ProjectItems.AddFolder("ViewModels");
  19:  
  20:    foreach (string node in frmORMappingWindow.SelectedTables)
  21:    {
  22:        string ClassName = node.Replace(" ", "_");
  23:        ClassDef clsDef = new ClassDef();
  24:        SQLStore store = new SQLStore();
  25:        string ClassDefined = ClassDef.GetClassTemplate;
  26:        ClassDefined = ClassDefined.Replace("$(NAMESPACE_DEF)$", string.Format("{0}.Models.ViewModels", project.Name));
  27:        ClassDefined = ClassDefined.Replace("$(CLASS_DEF)$", clsDef.GetClassDef(store.GetNoDataDataTableByName(string.Format("[{0}]", node)), ClassName));
  28:        //產生等會使用的暫存檔名
  29:        string TempCSPath = Path.Combine(
  30:            Environment.GetEnvironmentVariable("temp"),
  31:            string.Format("{0}.cs", ClassName));
  32:        //建立暫存的 Class 檔案
  33:        CreateModelCSFile(ClassDefined, TempCSPath);
  34:        //加入暫存的 Class 檔案
  35:        viewFolder.ProjectItems.AddFromFileCopy(TempCSPath);
  36:        //刪除掉暫存檔案
  37:        try {
  38:            File.Delete(TempCSPath);
  39:        }
  40:        catch (Exception ex) { } //刪除暫存檔案若失敗不處理任何訊息.
  41:    }

 

程式碼的部分其實並不會非常複雜,都是基本的處理而已,在 ClassDef & SQLStore 物件中資料來源都從前面第 (5) 建立資料來源中 而來。

 

步驟四

建立精靈主視窗介面,如下:

image

 

步驟五

建立主 Key 條件 的設定視窗,這個視窗主要建立前面步驟中所使用到的三個 static 變數 SelectedTableName、SelectedKey、SelectedTableColumns,如下:

image

 

三個變數相關說明如下:

SelectedTableName

存放畫面選擇的主要資料表名稱。


SelectedKey

存放選擇要當作主 Key 的名稱。


SelectedTableColumns

存放所有要拉進專案中的資料表串列。

 

資料表欄位設定的主視窗設計如下:

image

這邊 確定 的程式碼非常的簡單,其目的就是更新那三個 static 變數的值。

   1:      private void btnOK_Click(object sender, EventArgs e)
   2:    {
   3:        if (SelectedTableName == string.Empty)
   4:        {
   5:            MessageBox.Show("請選擇一個要當作主要資料存取的資料表。", this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
   6:            return;
   7:        }
   8:        if (SelectedKey == string.Empty)
   9:        {
  10:            if (lbxColumns.SelectedItem != null)
  11:            {
  12:                DialogResult dialog = MessageBox.Show(
  13:                    string.Format("是否要使用 \"{0}\" 當作主要條件?。", lbxColumns.SelectedItem.ToString()),
  14:                    this.Text,
  15:                    MessageBoxButtons.OKCancel,
  16:                    MessageBoxIcon.Information);
  17:  
  18:                if (dialog == DialogResult.OK)
  19:                {
  20:                    SelectedKey = lbxColumns.SelectedItem.ToString();
  21:                    Close();
  22:                }
  23:            }
  24:            else
  25:            {
  26:                MessageBox.Show("請選擇一個要當作主要資料存取的資料表。", this.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
  27:            }
  28:            return;
  29:        }
  30:        this.DialogResult = DialogResult.OK;
  31:        Close();
  32:    }

 

 

到這裡讀者一定非常好奇執行結果為何?下面就使用 GIF 動畫來呈現,這樣應該會比較有感覺。

RecordWistronTemplate

 

結語:

透過自定義範本精靈,我們可以將自行設計的架構透過產生器的方式,快速產生我們所需要的架構的程式碼,不僅加快開發的效率,也減低人為的錯誤率,也更一致化。未來筆者還會加入更多功能,有機會再分享給大家。

 

參考資料:

How to: Use Wizards with Project Templates

IWizard Interface


 

簽名:

學習是一趟奇妙的旅程

這當中,有辛苦、有心酸、也有成果。有時也會有瓶頸。要能夠繼續勇往直前就必須保有一顆最熱誠的心。

軟體開發之路(FB 社團)https://www.facebook.com/groups/361804473860062/

Gelis 程式設計訓練營(粉絲團)https://www.facebook.com/gelis.dev.learning/


 

如果文章對您有用,幫我點一下讚,或是點一下『我要推薦,這會讓我更有動力的為各位讀者撰寫下一篇文章。

非常謝謝各位的支持與愛護,小弟在此位各位說聲謝謝!!! ^_^