前言
這一篇是VS插件基於Visual Studio SDK擴展開發的,可能有些朋友看到【生成實體】心里可能會暗想,T4模板都可以做了、動軟不是已經做了么、不就是讀庫保存文件到指定路徑么……
我希望做的效果是:
1.工具集成到vs上
2.動作完成后體現到項目(添加、刪除項目項)
3.使用簡單、輕量、靈活(配置化)
4.不依賴ORM(前兩點有點像EF的DBFirst吧?)
文章最后會給上源碼地址。
下面是效果圖:
處理流程
以上是完整處理流程,我打算選擇部分流程來講。如果有對Visual Studio Package開發還沒一個認識,可以看我之前寫的一篇《Visual Studio Package 插件開發》。
按鈕的位置
從上圖看見,按鈕是在選中項目右鍵彈出的菜單欄里。
打開vsct文件,修改Group的Parent節點,修改對應的guid和id。
之前那邊文章有提到在文件:您的vs安裝目錄\VisualStudio2013\VSSDK\VisualStudioIntegration\Common\Inc\vsshlids.h 可以找到需要修改的名稱,但是右鍵是沒有在文件里定義,因此我們需要另外換一種方法。
1、打開注冊表編輯器(打開運行窗口,輸入regedit),
2、路徑[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\12.0\General],
3、右擊-新建-DWORD(32-位)值(D),其命名為EnableVSIPLogging
4、並將其值改為1。重啟VS,打開項目
5、按下Ctrl+Shift,對項目點擊右鍵,就會彈出窗口(如下圖)
Guid和CmdID的值就是我們需要的,在vsct文件Symbols節點添加GuidSymbol項,value上圖的{D309F791-903F-11D0-9EFC-00A0C911004F},IDSymbol項value為1026。
最后在Group的Parent節點的屬性guid和id改為與上面對應,下面代碼為例子。
PS:上面方法有點久遠了,現在2017、2019可以用新的方式來查找需要的功能guid與cmdID。
非常感謝yanusosu兄弟的貢獻。

<CommandTable xmlns="http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <Commands package="guidAutoBuildEntityPkg"> <Groups> <Group guid="guidAutoBuildEntityCmdSet" id="MyMenuGroup" priority="0x0600"> <Parent guid="guidCodeWindowRightClickCmdSet" id="CodeWindowRightClickMenu"/> </Group> </Groups> </Commands> <Symbols> <GuidSymbol name="guidAutoBuildEntityPkg" value="{c095f8f8-3f87-4eac-8dc0-44939a85b2f2}" /> <GuidSymbol name="guidCodeWindowRightClickCmdSet" value="{D309F791-903F-11D0-9EFC-00A0C911004F}"> <IDSymbol name="CodeWindowRightClickMenu" value="1026" /> </GuidSymbol> </Symbols> </CommandTable>
讀取選中項目信息
重點是DTE 接口的使用,MSDN的描述是:DTE 接口Visual Studio 自動化對象模型中的頂級對象。強大到當前開發環境中任何屬性可以拿到例如:當前打開的文檔集合,解決方案下的項目信息……剩下自己看,傳送門
下面是代碼示例:

var dte = (DTE)GetService(typeof(SDTE)); /// <summary> /// 獲取選中項目的信息 /// </summary> /// <param name="dte"></param> /// <returns></returns> public static SelectedProject GetSelectedProjectInfo(this DTE dte) { var selectedItems = dte.SelectedItems; var projectName = (from SelectedItem item in selectedItems select item.Name).ToList(); if (!selectedItems.MultiSelect && selectedItems.Count == 1) { var selectProject = selectedItems.Item(projectName.First()); var projectFileList = (from ProjectItem projectItem in selectProject.Project.ProjectItems where projectItem.Name.EndsWith(".cs") select Path.GetFileNameWithoutExtension(projectItem.Name)).ToList(); return new SelectedProject(selectProject.Project.FullName, selectProject.Project, projectFileList); } return null; }
讀取實體配置信息
配置存放兩點信息:數據庫連接、類文件模版,同時我們約定存放在項目根目錄下。如下圖
那么,剩下就是XML的基本獲取處理了。__entity.xml的模版在源碼里,可自行拷貝去需要使用的項目,以下是代碼示例:

private void AutoBuildEntityEvent(object sender, EventArgs e) { var autoBuildEntityContent = new AutoBuildEntityContent (); //讀取選中項目下的配置信息 var entityXmlModel = new EntityXml(autoBuildEntityContent.SelectedProject.EntityXmlPath); entityXmlModel.Load(); autoBuildEntityContent.EntityXml = entityXmlModel; new MainForm(autoBuildEntityContent).ShowDialog(); } public class EntityXml { private readonly string _path; public EntityXml(string path) { _path = path; } public string ConnString { get; private set; } public string EntityTemplate { get; private set; } /// <summary> /// 讀取_entity.xml /// </summary> /// <returns></returns> public EntityXml Load() { var xml = new XmlDocument(); xml.Load(_path); var autoEntityNode = xml.SelectSingleNode("AutoEntity"); if (autoEntityNode != null) { var connStringNode = autoEntityNode.SelectSingleNode("ConnString"); if (connStringNode != null) { ConnString = connStringNode.InnerText; } var templatesNodes = autoEntityNode.SelectSingleNode("Template"); if (templatesNodes != null) { EntityTemplate = templatesNodes.InnerText; } } return this; } }
讀取物理表
查詢當前數據庫的表集合,傳給窗體做列表展示,直接上代碼:

/// <summary> /// 物理表 /// </summary> public class DbTable { public string TableName { get; private set; } public List<TableColumn> Columns { get; set; } private readonly string _conn; public DbTable(string conn) { _conn = conn; } public DbTable(string tableName, List<TableColumn> columns) { TableName = tableName; Columns = columns; } public List<string> QueryTablesName() { var result = SqlHelper.Query(_conn, @"SELECT name FROM sysobjects WHERE xtype IN ( 'u','v' ); "); return (from DataRow row in result.Rows select row[0].ToString()).ToList(); } public List<DbTable> GetTables(List<string> tablesName) { if (!tablesName.Any()) return new List<DbTable>(); var t = new TableColumn(_conn); var columns = t.QueryColumn(tablesName); return columns.GroupBy(a => a.TableName).Select(a => new DbTable(a.Key, a.ToList())).ToList(); } }
讀取表結構
選擇響應的表后,查詢出對應的表結構,一般實體的所需要的信息有:列名、列備注、類型、長度、是否主鍵、是否自增長、是否可空,繼續上代碼:

/// <summary> /// 物理表的列信息 /// </summary> public class TableColumn { private readonly string _connStr; public TableColumn() { } public TableColumn(string connStr) { _connStr = connStr; } public string TableName { get; private set; } public string Name { get; private set; } public string Remark { get; private set; } public string Type { get; private set; } public int Length { get; private set; } public bool IsIdentity { get; private set; } public bool IsKey { get; private set; } public bool IsNullable { get; private set; } public string CSharpType { get { return SqlHelper.MapCsharpType(Type, IsNullable); } } /// <summary> /// 查詢列信息 /// </summary> /// <param name="tablesName"></param> /// <returns></returns> public List<TableColumn> QueryColumn(List<string> tablesName) { #region 表結構 var paramKey = string.Join(",", tablesName.Select((a, index) => "@p" + index)); var paramVal = tablesName.Select((a, index) => new SqlParameter("@p" + index, a)).ToArray(); var sql = string.Format(@"SELECT obj.name AS tablename , col.name , ISNULL(ep.[value], '') remark , t.name AS type , col.length , COLUMNPROPERTY(col.id, col.name, 'IsIdentity') AS isidentity , CASE WHEN EXISTS ( SELECT 1 FROM dbo.sysindexes si INNER JOIN dbo.sysindexkeys sik ON si.id = sik.id AND si.indid = sik.indid INNER JOIN dbo.syscolumns sc ON sc.id = sik.id AND sc.colid = sik.colid INNER JOIN dbo.sysobjects so ON so.name = si.name AND so.xtype = 'PK' WHERE sc.id = col.id AND sc.colid = col.colid ) THEN 1 ELSE 0 END AS iskey , col.isnullable FROM dbo.syscolumns col LEFT JOIN dbo.systypes t ON col.xtype = t.xusertype INNER JOIN dbo.sysobjects obj ON col.id = obj.id AND obj.xtype IN ( 'U', 'v' ) AND obj.status >= 0 LEFT JOIN dbo.syscomments comm ON col.cdefault = comm.id LEFT JOIN sys.extended_properties ep ON col.id = ep.major_id AND col.colid = ep.minor_id AND ep.name = 'MS_Description' LEFT JOIN sys.extended_properties epTwo ON obj.id = epTwo.major_id AND epTwo.minor_id = 0 AND epTwo.name = 'MS_Description' WHERE obj.name IN ({0});", paramKey); #endregion var result = SqlHelper.Query(_connStr, sql, paramVal); return (from DataRow row in result.Rows select new TableColumn { IsIdentity = Convert.ToBoolean(row["isidentity"]), IsKey = Convert.ToBoolean(row["iskey"]), IsNullable = Convert.ToBoolean(row["isnullable"]), Length = Convert.ToInt32(row["length"]), Name = row["name"].ToString(), Remark = row["remark"].ToString(), TableName = row["tablename"].ToString(), Type = row["type"].ToString() }).ToList(); } }
根據模板生成代碼
開始我是嘗試用T4的,發現不方便,繁雜的聲明。因此我選擇了nVelocity,這里不做太多介紹,附上相關文章學習,傳送門

// <summary> /// 初始化模板引擎 /// </summary> public static string ProcessTemplate(string template, Dictionary<string, object> param) { var templateEngine = new VelocityEngine(); templateEngine.SetProperty(RuntimeConstants.RESOURCE_LOADER, "file"); templateEngine.SetProperty(RuntimeConstants.INPUT_ENCODING, "utf-8"); templateEngine.SetProperty(RuntimeConstants.OUTPUT_ENCODING, "utf-8"); templateEngine.SetProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, AppDomain.CurrentDomain.BaseDirectory); var context = new VelocityContext(); foreach (var item in param) { context.Put(item.Key, item.Value); } templateEngine.Init(); var writer = new StringWriter(); templateEngine.Evaluate(context, writer, "mystring", template); return writer.GetStringBuilder().ToString(); }
之前已經拿到的文件模版,通過上面的方法輸出類文本,保存到選中項目的根目錄下。

public static class FilesHelper { public static string Write(string directory, string fileName, string content) { var path = Path.Combine(directory, fileName + ".cs"); using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write)) { byte[] byteFile = Encoding.UTF8.GetBytes(content); fs.Write(byteFile, 0, byteFile.Length); } return path; } }
操作項目
終於到了最后一步了,部分人以為保存了文件后就完事了,最后通過包含文件就完事了。我們還是有點追求的,既然做成了插件就要更加的方便化。
通過之前[讀取選中項目信息]步驟拿到的EnvDTE.Project ProjectDte,使用以下擴展方法進行添加、刪除項目項。

/// <summary> /// 添加項目項 /// </summary> /// <param name="projectDte"></param> /// <param name="files"></param> public static void AddFilesToProject(this Project projectDte, List<string> files) { foreach (string file in files) { projectDte.ProjectItems.AddFromFile(file); } if (files.Any()) projectDte.Save(); } /// <summary> /// 排除項目項 /// </summary> /// <param name="projectDte"></param> /// <param name="files"></param> public static void RemoveFilesFromProject(this Project projectDte, List<string> files) { foreach (string file in files) { projectDte.ProjectItems.Item(Path.GetFileName(file)).Remove(); } if (files.Any()) projectDte.Save(); }
附加
部分同學可能想調試的時候會出現:無法直接啟動“類庫輸出類型”項目,可以在項目屬性-調試配置:
1.啟動配置外部程序:C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\devenv.exe
2.命令行參數:/rootsuffix Exp
估計有同學會制作自己的圖標,另外附上兩條icon制作的網站:
http://iconfont.cn/search/index
http://www.easyicon.net/covert/
結尾
整篇文章的技術難點並不多,但是因為插件開發的資料相對較少,80%的時間花去找接口文檔、找資料。
此工具的原型是公司架構師的,公司所有開發都在用,但是他把源碼丟了………………好奇心使我重新實現了一份,當然了,說不定哪天帶團隊的時候會用上。
最后雙手奉上源碼,並不是什么牛逼的東西,希望可以幫助需要的同學。https://github.com/SkyChenSky/AutoBuildEntity
如果本篇文章對您有幫助,可以點擊左下角的推薦,這是給我最大的鼓勵,如果有什么建議和優化,可以在下面評論提出,謝謝。