C#發現之旅第三講 使用C#開發基于XSLT的代碼生成器
袁永福 2008-5-15
系列課程說明
為了讓大家更深入的了解和使用C#,我們開始這一系列的主題為“C#發現之旅”的技術講座??紤]到各位大多是進行WEB數據庫開發的,而所謂發現就是發現我們所不熟悉的領域,因此本系列講座內容將是C#在WEB數據庫開發以外的應用。目前規劃的主要內容是圖形開發和XML開發,并計劃編排了多個課程。在未來的C#發現之旅中,我們按照由淺入深,循序漸進的步驟,一起探索和發現C#的其他未知的領域,更深入的理解和掌握使用C#進行軟件開發,拓寬我們的視野,增強我們的軟件開發綜合能力。
本系列課程配套的演示代碼下載地址為 https://files.cnblogs.com/xdesigner/cs_discovery.zip 。
本課程說明 經過以前的學習,我們大體上了解了XML/XSLT開發,在本課程中,我們將在ASP.NET中使用C#開發一個基于XSLT技術的代碼生成器。
本系列課程已發布的文章有
C#發現之旅第一講 C#-XML開發
C#發現之旅第二講 C#-XSLT開發
C#發現之旅第三講 使用C#開發基于XSLT的代碼生成器
C#發現之旅第四講 Windows圖形開發入門
C#發現之旅第五講 圖形開發基礎篇
C#發現之旅第六講 C#圖形開發中級篇
C#發現之旅第七講 C#圖形開發高級篇
C#發現之旅第八講 ASP.NET圖形開發帶超鏈接的餅圖
C#發現之旅第九講 ASP.NET驗證碼技術
C#發現之旅第十講 文檔對象模型
首先說說什么是代碼生成器。個人認為是一種工具軟件,它能根據某種已經固定的信息,使用程序來機械的大批量的生成有結構上有比較簡單規律的源代碼,從而減少軟件開發人員的編碼量。
從廣義上講,我們寫的WEB數據庫程序都是代碼生成器,它們能根據保存在數據庫中的固定數據自動生成大量的HTML代碼。在這里我們限制代碼生成器為通用代碼生成器。代碼生成器主要功能是幫助程序員自動生成大量的底層代碼,這種代碼可以是C#或Java的程序源代碼,也可以是SQL語句,或者HTML代碼等等,是一種軟件開發過程中的輔助工具軟件。
我們最常用的代碼生成器是根據數據庫結構自動生成能操作數據庫記錄的程序源代碼,SQL語句或其他文檔等等。對于這種代碼生成器,其數據信息來源就是數據庫的表結構和字段屬性等信息,我們可以分析遍歷數據庫的系統表來貨的表結構和字段信息,也可以從PowerDesigner等數據結構設計器保存的文檔中獲得。
針對某個特定的項目,我們可以根據數據庫結構臨時寫一個代碼生成器,使用字符串拼湊來生成源代碼,但這種代碼生成器不通用,難于用于其他項目。因此我們更多的是使用通用的代碼生成器。
很多通用代碼生成器的原理如圖

在這個圖中,我們看到代碼生成器包含了模板庫和代碼生成器處理引擎,模板庫包含了若干個模板,一個模板一般是純文本,其中可能包含了腳本代碼,或者類似ASP的結構。生成器引擎加載一個或者多個數據庫表結構設計信息,然后調用用戶指定的模板,通過某種操作來自動生成另外一個文本文件,這個文本文件內容可以是純文本,HTML代碼,C#代碼或者其他。
考察這個結構,可以發現這個原理和XSLT原理很相似。我們可以將數據庫表結構設計信息保存在一個XML文檔中,代碼生成器模板就使用XSLT格式,代碼生成器引擎就使用XSLT轉換引擎,這樣我們也可以達到代碼生成器的功能,從而搞出基于XSLT的代碼生成器。

軟件設計
根據基于XSLT來實現代碼生成功能的思路,我們開始設計這個代碼生成器。
數據來源這個代碼生成器的數據來源就是數據庫表結構設計信息。對于不同的數據庫類型得使用不同的方法來獲得其表結構設計信息。在此我們針對于Access2000,MSSQLServer和Oracle這三種數據庫研究獲得其表設計信息的方法,對于其他類型的數據庫則以后再說。
對于MSSQLServer,數據庫中有SysObjects和SysColumns這兩個系統表,我們可以查詢系統表來獲得所有的表名稱和字段名稱以及格式,還有一個sp_helpindex 的系統預定義存儲過程來獲得指定表的字段索引信息。
對于Oracle,數據庫有一個名為Col的系統預定義視圖,我們可以查詢這個視圖獲得所有的表名和字段定義信息。還有一個 user_ind_columns的系統預定義視圖,我們可以關鍵字段信息。
對于Access2000數據庫,沒有這些系統表,因此我們使用。NET框架中的OleDB的數據連接對象的GetOleDbSchemaTable函數來獲得數據庫表和字段定義信息。
我們的代碼生成器還應當從一下數據結構設計器生成的文檔導入表結構設計信息,這里我們決定從PowerDesigner生成的PDM文件中導入表設計信息,因為PDM文件是XML格式的,處理方便。
代碼生成模板這里的代碼生成模板就采用XSLT格式。為了方便軟件開發和維護,我們將模板保存在程序目錄下的XSLT擴展名的文件中,并約定文件名使用下劃線開頭。
程序在這里使用ASP.NET中實現該代碼生成器,使用.NET框架自帶的XSLT轉換對象來作為代碼生成器處理引擎,并使用HTML格式來展示生成的源代碼。
程序說明根據上述的軟件設計,我們開發出了這個代碼生成器,現對其源代碼進行詳細說明。
xslcreatecode.aspx 本代碼生成器很簡單,只有一個ASPX頁面,打開該頁面的設計界面,

可以看到上面放置了一些簡單的控件。其中比較重要的有
數據表名下拉列表,該列表列出了數據庫中所有數據表的名稱。
XSLT模板名稱下拉列表,該列表列出了所有系統可用的XSLT模板文件的名稱。
刷新系統按鈕,用于刷新系統數據設置,重新填充數據表名列表和模板名列表。
創建代碼,根據當前選擇的數據表名和XSLT模板名稱創建代碼。
生成的源代碼文本標簽,使用HTML格式來顯示生成的源代碼。
打開這個頁面的C#代碼,可以看到其代碼也不復雜。這個頁面的Page_Load函數調用了刷新系統的方法RefreshSystem。
| /// <summary> /// 刷新系統 /// </summary> private void RefreshSystem( ) { DataBaseInfo info = this.GetInfo(); this.lblResult.Text = info.Name ; if( cboTable.Items.Count == 0 ) { cboTable.Items.Add( new ListItem("所有表" , "所有表" )); foreach( TableInfo table in info.Tables ) { cboTable.Items.Add( new ListItem( table.Name , table.Name )); } } if( cboXSLT.Items.Count == 0 ) { cboXSLT.Items.Add("XML代碼"); string[] names = System.IO.Directory.GetFiles( this.MapPath(".") , "_*.xslt"); if( names != null && names.Length > 0 ) { foreach( string name in names ) { string name2 = System.IO.Path.GetFileNameWithoutExtension( name ); this.cboXSLT.Items.Add( new ListItem( name2 , name2 )); } } } }//private void RefreshSystem( ) /// <summary> /// 獲得數據庫結構信息對象 /// </summary> /// <returns>數據庫結構信息對象</returns> private DataBaseInfo GetInfo( ) { DataBaseInfo info = this.Session["info"] as DataBaseInfo ; if( info == null ) { info = new DataBaseInfo(); info.LoadFromAccess2000( this.MapPath("demomdb.mdb")); this.Session["info"] = info ; } return info ; } |
在RefreshSystem方法中,首先獲得數據庫結構信息對象,遍歷其中的表結構信息對象,向數據表名下列列表填充項目。
遍歷網站目錄下的所有以下劃線開頭的XSLT文件,將其文件名填充到XSLT模板下拉列表中。
這里使用了另外一個函數GetInfo,該函數就是獲得系統使用的數據庫結構信息對象,它是緩存在session中的對象,它使用了程序目錄下的演示數據庫作為數據結構信息來源。
頁面代碼中還有強制刷新系統按鈕事件處理。
| /// <summary> /// 刷新系統按紐事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void cmdRefresh_Click(object sender, System.EventArgs e) { this.Session["info"] = null; this.cboTable.Items.Clear(); this.cboXSLT.Items.Clear(); RefreshSystem( ); }//private void cmdRefresh_Click(object sender, System.EventArgs e) |
這個處理過程比較簡單,將緩存的數據結構信息對象刪除掉,清空數據表名列表和模板列表,然后調用RefreshSystem方法刷新界面。
這個頁面最重要的代碼就是自動生成并顯示代碼的過程了。其C#代碼為
| mmary> /// 創建代碼按紐事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void cmdCreate_Click(object sender, System.EventArgs e) { DataBaseInfo info = this.GetInfo(); string xml = null ; if( cboTable.SelectedIndex == 0 ) { xml = GetXMLString( info ); } else { TableInfo table = info.Tables[ this.cboTable.SelectedValue ] ; if( table == null ) { this.lblResult.Text = "請選擇一個表"; return ; } xml = GetXMLString( table ); } string html = ""; if( cboXSLT.SelectedIndex <= 0 ) { // 沒有使用任何模板,直接顯示XML源代碼 html = @"<textarea wrap=off readonly style='border:1 solid black; overflow=visible; background-color:#dddddd'>" + xml + "</textarea>"; } else { // 啟動了XSLT模板,執行XSLT轉換 System.Xml.Xsl.XslTransform transform = new System.Xml.Xsl.XslTransform(); transform.Load( this.Server.MapPath( this.cboXSLT.SelectedValue ) + ".xslt" ); System.IO.StringWriter writer = new System.IO.StringWriter(); System.Xml.XmlDocument doc = new System.Xml.XmlDocument(); doc.LoadXml( xml ); transform.Transform( doc , null , writer , null ); writer.Close(); html = writer.ToString(); } this.lblResult.Text = "<b>共生成 " + html.Length + " 個字符</b><br />\r\n" + html ; } |
在生成代碼按鈕事件處理中,首先根據數據表名列表獲得當前數據表結構信息對象,并生成XML字符串。若用戶指定了某個數據表則調用GetXMLString函數來生成該數據表的XML字符串,否則對整個數據結構信息對象生成XML字符串。
若用戶沒有指定XSLT模板,則生成直接顯示XML代碼的HTML代碼。這里使用textarea元素來顯示XML代碼,這樣XML代碼顯示時不需要進行轉義處理。
若用戶指定了XSLT模板名稱,則創建一個XslTransform對象,使用Load方法從程序目錄中加載該名稱的XSLT模板文件。這里可以看出我們的程序需要訪問程序目錄的權限,因此部署這個代碼生成器時是需要配置權限的,使得程序能訪問程序所在目錄。
程序還創建一個XmlDocument對象,調用它的LoadXml方法來加載剛剛生成的XML字符串,然后執行XSLT轉換,轉換結果就當作HTML代碼準備顯示了。
最后程序根據生成的HTML代碼設置到一個標簽控件中。
代碼中還定義了一個GetXMLString函數,該函數能將一個對象序列化成一個帶縮進的XML字符串。其內部使用了XmlSerializer對象。使用XmlSerializer對象能很方便的將對象的公開屬性導出為XML文檔。
| /// <summary> /// 將指定對象序列化成XML文檔,然后返回獲得的XML字符串 /// </summary> /// <param name="obj">對象</param> /// <returns>XML字符串</returns> private string GetXMLString( object obj ) { System.IO.StringWriter myStr = new System.IO.StringWriter(); System.Xml.XmlTextWriter writer = new System.Xml.XmlTextWriter( myStr ); writer.Indentation = 3 ; writer.IndentChar = ' '; writer.Formatting = System.Xml.Formatting.Indented ; System.Xml.Serialization.XmlSerializer sc = new System.Xml.Serialization.XmlSerializer( obj.GetType() ); sc.Serialize( writer , obj ); writer.Close(); string xml = myStr.ToString(); int index = xml.IndexOf("?>"); if( index > 0 ) xml = xml.Substring( index + 2 ); return xml.Trim() ; } |
DataBaseInfo.cs
本代碼生成器包括了DataBaseInfo.cs文件,其中就是比較底層的描述數據庫表結構和字段設計信息的對象。這個文件中定義了多個類型,其中DataBaseInfo表示一個數據庫,TableInfo表示數據表設計信息可,FieldInfo表示數據庫字段設計信息。這三個類型構成了一個數據庫-數據表-字段的三層對象樹狀結構。TableInfo和FieldInfo的代碼不復雜?,F在說一下DataBaseInfo類型中的一些代碼。
首先看看LoadFromPDMXMLFile和LoadFromPDMXMLDocument函數,這個函數能分析PDM格式的XML文檔,找出其中的數據表和字段設計信息并填充到內部結構中。某些版本的PowerDesigner生成的PDM文件是XML格式的,這方便其他程序能加載其中的數據結構設計信息。關于這個函數詳細的處理過程,大家可以參考一個PDM的文件仔細分析。
| /// <summary> /// 從PDM數據結構定義XML文件中加載數據結構信息 /// </summary> /// <param name="doc">XML文檔對象</param> /// <returns>加載的字段信息個數</returns> public int LoadFromPDMXMLDocument( XmlDocument doc ) { intFillStyle = FillStyleConst.PDM ; int RecordCount = 0 ; myTables.Clear(); XmlNamespaceManager nsm = new XmlNamespaceManager( doc.NameTable ); nsm.AddNamespace( "a" , "attribute" ); nsm.AddNamespace( "c" , "collection" ); nsm.AddNamespace( "o" , "object"); XmlNode RootNode = doc.SelectSingleNode("/Model/o:RootObject/c:Children/o:Model" , nsm ); if( RootNode == null ) return 0 ; strName = ReadXMLValue( RootNode , "a:Name" , nsm ); strDescription = strName ; // 數據表 foreach( XmlNode TableNode in RootNode.SelectNodes("c:Tables/o:Table" , nsm )) { TableInfo table = new TableInfo(); myTables.Add( table ); table.Name = ReadXMLValue( TableNode , "a:Code" , nsm ); table.Remark = ReadXMLValue( TableNode , "a:Name" , nsm ); string keyid = ReadXMLValue( TableNode , "c:PrimaryKey/o:Key/@Ref" , nsm ); System.Collections.Specialized.StringCollection Keys = new System.Collections.Specialized.StringCollection(); if( keyid != null ) { foreach( XmlNode KeyNode in TableNode.SelectNodes( "c:Keys/o:Key[@Id = '" + keyid + "']/c:Key.Columns/o:Column/@Ref" , nsm )) { Keys.Add( KeyNode.Value ); } } foreach( XmlNode FieldNode in TableNode.SelectNodes("c:Columns/o:Column" , nsm )) { RecordCount ++ ; string id = ( ( XmlElement ) FieldNode).GetAttribute("Id"); FieldInfo field = new FieldInfo(); table.Fields.Add( field ); field.Name = ReadXMLValue( FieldNode , "a:Code" , nsm ); field.Remark = ReadXMLValue( FieldNode , "a:Name" , nsm ); field.Description = ReadXMLValue( FieldNode , "a:Comment" , nsm ); string FieldType = ReadXMLValue( FieldNode , "a:DataType" , nsm ); if( FieldType != null ) { int index = FieldType.IndexOf("("); if( index > 0 ) FieldType = FieldType.Substring( 0 , index ); } field.FieldType = FieldType ; field.FieldWidth = ReadXMLValue( FieldNode , "a:Length" , nsm ); if( Keys.Contains( id )) field.PrimaryKey = true; } } return RecordCount ; } private string ReadXMLValue( System.Xml.XmlNode node , string path , System.Xml.XmlNamespaceManager nsm ) { System.Xml.XmlNode node2 = node.SelectSingleNode( path , nsm ); if( node2 == null ) return null ; else { if( node2 is System.Xml.XmlElement ) return ( ( System.Xml.XmlElement ) node2).InnerText ; else return node2.Value ; } } |
LoadFromAccess2000函數能從一個Access2000格式的數據庫中加載數據結構設計信息。這個函數內部使用了OleDbConnection對象的GetOleDbSchemaTable方法可以獲得數據庫的一些信息,具體什么樣式可以參考MSND中關于GetOleDbSchema方法的詳細說明。對于不同的數據庫其處理過程是不同的,在這里的使用方法是我經過反復測試得到的,而且只適用于Access2000數據庫。在這里首先是獲得所有的數據表名和字段設計信息,然后獲得字段索引信息。
| /// <summary> /// 從 Jet40( Access2000 ) 的數據庫中加載數據結構信息 /// </summary> /// <param name="myConn">數據庫連接對象</param> /// <returns>加載的字段信息個數</returns> public int LoadFromAccess2000( OleDbConnection myConn ) { intFillStyle = FillStyleConst.Access2000 ; int RecordCount = 0 ; myTables.Clear(); string dbName = myConn.DataSource ; if( dbName != null ) strName = System.IO.Path.GetFileName( dbName ); using(System.Data.DataTable myDataTable = myConn.GetOleDbSchemaTable( System.Data.OleDb.OleDbSchemaGuid.Columns , null)) { foreach( System.Data.DataRow myRow in myDataTable.Rows ) { string strTable = Convert.ToString( myRow["TABLE_NAME"] ); if( ! strTable.StartsWith("MSys")) { TableInfo myTable = myTables[ strTable ] ; if( myTable == null ) { myTable = new TableInfo(); myTable.Name = strTable ; myTables.Add( myTable ); } FieldInfo myField = new FieldInfo(); myTable.Fields.Add( myField ); myField.Name = Convert.ToString( myRow["COLUMN_NAME"]); myField.Nullable = Convert.ToBoolean( myRow["IS_NULLABLE"]); System.Data.OleDb.OleDbType intType = (System.Data.OleDb.OleDbType) Convert.ToInt32( myRow["DATA_TYPE"]); if( System.DBNull.Value.Equals( myRow["DESCRIPTION"] ) == false ) { myField.Remark = Convert.ToString( myRow["DESCRIPTION"] ) ; } if( intType == System.Data.OleDb.OleDbType.WChar ) { myField.FieldType = "Char" ; } else { myField.FieldType = intType.ToString(); } myField.FieldWidth = Convert.ToString( myRow["CHARACTER_MAXIMUM_LENGTH"]); RecordCount ++ ; } }//foreach }//using using( System.Data.DataTable myDataTable = myConn.GetOleDbSchemaTable( System.Data.OleDb.OleDbSchemaGuid.Indexes , null)) { foreach( System.Data.DataRow myRow in myDataTable.Rows ) { string strTable = Convert.ToString( myRow["TABLE_NAME"] ); TableInfo myTable = myTables[ strTable ]; if( myTable != null ) { FieldInfo myField = myTable.Fields[ Convert.ToString( myRow["COLUMN_NAME"])]; if( myField != null) { myField.Indexed = true; myField.PrimaryKey = ( Convert.ToBoolean( myRow["PRIMARY_KEY"])); } } }//foreach }//using return RecordCount ; }//public int LoadFromAccess2000( OleDbConnection myConn ) |
LoadFromOracle函數用于分析Oracle數據庫而獲得表結構和字段設計信息。其代碼如下。
在ORACLE數據庫中,有一個名為COL的系統預定義視圖,里面就是各個數據表名和字段定義信息,還有一個名為user_ind_columns的預定義視圖,里面就保存著字段索引信息。
我們首先查詢遍歷COL視圖,獲得該視圖中保存的數據表名,字段名,字段數據類型,字段長度等信息。建立起基本的表和字段信息結構,然后查詢遍歷user_ind_columns視圖,獲得其關鍵字段信息。
| /// <summary> /// 從 Oracle 加載數據庫結構信息 /// </summary> /// <param name="myConn">數據庫連接對象</param> /// <returns>加載的字段信息個數</returns> public int LoadFromOracle( IDbConnection myConn ) { intFillStyle = FillStyleConst.Oracle ; int RecordCount = 0 ; string strSQL = null; strSQL = "Select TName,CName,coltype,width From Col Order by TName,CName"; myTables.Clear(); if( myConn is OleDbConnection ) { strName = ( ( System.Data.OleDb.OleDbConnection ) myConn ).DataSource + " - " + myConn.Database ; } else strName = myConn.Database ; using( System.Data.IDbCommand myCmd = myConn.CreateCommand()) { myCmd.CommandText = strSQL ; IDataReader myReader = myCmd.ExecuteReader( CommandBehavior.SingleResult ); TableInfo LastTable = null; while( myReader.Read()) { string TableName = myReader.GetString(0).Trim(); if( LastTable == null || LastTable.Name != TableName ) { LastTable = new TableInfo(); myTables.Add( LastTable ); LastTable.Name = TableName ; } FieldInfo NewField = new FieldInfo(); LastTable.Fields.Add( NewField ); NewField.Name = myReader.GetString(1); NewField.FieldType = myReader.GetString(2); NewField.FieldWidth = myReader[3].ToString(); RecordCount ++ ; }//while myReader.Close(); myCmd.CommandText = @" select table_name , column_name , index_name from user_ind_columns order by table_name , column_name "; myReader = myCmd.ExecuteReader( CommandBehavior.SingleResult ); TableInfo myTable = null; while( myReader.Read()) { myTable = myTables[ myReader.GetString(0)]; if( myTable != null ) { string IDName = myReader.GetString(2); string FieldName = myReader.GetString(1); FieldInfo myField = myTable.Fields[ FieldName ]; if( myField != null ) { myField.Indexed = true ; if( IDName.StartsWith("PK") ) { myField.PrimaryKey = true; } } } }//while myReader.Close(); }//using return RecordCount ; }//public int LoadFromOracle( System.Data.IDbConnection myConn ) |
LoadFromSQLServer函數用于分析一個MSSQLServer數據庫,加載其表和字段設計信息。其代碼如下。在SQLSERVER中包含了一些系統表,比如SysObjects,SysColumns等等,里面就存儲了系統中所有對象的信息,比如表,字段,存儲過程,觸發器等等。我們就可以從這些系統表中查詢所有的表和字段定義信息。SQLSERVER中還有一個名為sp_helpindex的系統預定義存儲過程,可用來查詢指定表的索引信息。
在代碼中我們首先使用一個比較復雜的SQL語句從系統表中查詢數據庫中所有的數據表名,字段名,字段類型和長度等信息。這里的SQL語句是我個人摸索的,相信大家可以寫出更好更準確的SQL語句。我們讀取查詢結果就可以構造出基本的表和字段對象結構,然后針對每一個表對象,調用sp_helpindex存儲過程,獲得數據表中定義的關鍵字段信息。
| /// <summary> /// 從 SQLServer 中加載數據庫結構信息 /// </summary> /// <param name="myConn">數據庫連接對象</param> /// <returns>加載的字段信息個數</returns> public int LoadFromSQLServer( IDbConnection myConn ) { intFillStyle = FillStyleConst.SQLServer ; int RecordCount = 0 ; if( myConn is OleDbConnection ) strName = ( ( OleDbConnection ) myConn ).DataSource ; else if( myConn is System.Data.SqlClient.SqlConnection ) strName = ( ( System.Data.SqlClient.SqlConnection ) myConn ).DataSource ; strName = strName + " - " + myConn.Database ; string strSQL = null; strSQL = @" select sysobjects.name , syscolumns.name , systypes.name , syscolumns.length , syscolumns.isnullable , sysobjects.type from syscolumns, sysobjects, systypes where syscolumns.id=sysobjects.id and syscolumns.xusertype=systypes.xusertype and (sysobjects.type='U' or sysobjects.type='V' ) and systypes.name <>'_default_' and systypes.name<>'sysname' order by sysobjects.name, syscolumns.name"; myTables.Clear(); using( System.Data.IDbCommand myCmd = myConn.CreateCommand()) { myCmd.CommandText = strSQL ; IDataReader myReader = myCmd.ExecuteReader( CommandBehavior.SingleResult ); TableInfo LastTable = null; while( myReader.Read()) { string TableName = myReader.GetString(0).Trim(); if( LastTable == null || LastTable.Name != TableName ) { LastTable = new TableInfo(); myTables.Add( LastTable ); LastTable.Name = TableName ; LastTable.Tag = Convert.ToString( myReader.GetValue( 5 )); } FieldInfo NewField = new FieldInfo(); LastTable.Fields.Add( NewField ); NewField.Name = myReader.GetString(1); NewField.FieldType = myReader.GetString(2); NewField.FieldWidth = myReader[3].ToString(); if( myReader.IsDBNull( 4 ) == false) NewField.Nullable = (myReader.GetInt32(4) == 1); RecordCount ++ ; }//while myReader.Close(); // 加載主鍵信息 for( int iCount = myTables.Count - 1 ; iCount >= 0 ; iCount -- ) { TableInfo myTable = myTables[ iCount ] ; if( string.Compare( ( string ) myTable.Tag , "U" , true ) == 0 ) { try { myCmd.CommandText = "sp_helpindex \"" + myTable.Name + "\"" ; //myCmd.CommandType = System.Data.CommandType.Text ; myReader = myCmd.ExecuteReader( ); while( myReader.Read()) { string strKeyName = myReader.GetString(0); string strDesc = myReader.GetString(1); string strFields = myReader.GetString(2); bool bolPrimary = ( strDesc.ToLower().IndexOf("primary") >= 0 ); foreach( string strField in strFields.Split(',')) { FieldInfo myField = myTable.Fields[ strField.Trim()]; if( myField != null) { myField.Indexed = true; myField.PrimaryKey = bolPrimary ; } }//foreach }//while myReader.Close(); } catch( Exception ext ) { //this.List.Remove( myTable ); myTable.Name = myTable.Name + " " + ext.Message ; } } }//foreach }//using return RecordCount ; }//public int LoadFromSQLServer( System.Data.IDbConnection myConn ) |
目前DataBaseInfo對象只能分析Access2000,SQLSERVER和Oralce數據庫,大家以后可以完善它,使得它能分析比如DB2,MYSQL等其他數據庫類型。在未來的軟件開發過程中,若需要分析數據庫結構的,則只要調用這個DataBaseInfo就可以了。
在本演示程序中,我們只是用程序目錄下的一個Access2000數據庫作為例子,因此也只調用了LoadFromAccesss2000這個函數,其他的分析SQLSERVER和ORACLE的函數沒用到。在未來當這個代碼生成器經過改善而投入實際應用時,它就能分析SQLSERVER和ORACLE等企業級數據庫了。
在主頁面xslcreatecode.aspx中定義了一個GetXMLString函數,它能將一個對象序列化成一個XML文檔。這里的DataBaseInfo,TableInfo和FieldInfo都能XML序列化。在執行XML序列化時,系統會分析對象類型,遍歷對象所有的公開字段和可讀寫屬性,然后將這些屬性值輸出到XML文檔,若遇到對象樹狀結構,則會遞歸遍歷這個樹狀結構,對對象中的每一個下屬對象都會建立一個XML子元素進行輸出。在這里DataBaseInfo,TableInfo和FieldInfo構成了三層的樹狀結構,因此生成的XML文檔也是多層次的。
一般來說,序列化生成的XML文檔中,XML元素的名稱等于對象類型的名稱和公開字段屬性的名稱,但可以通過添加特性來改變這種默認行為,在類型TableInfo的定義前面加上了特性XmlType,在這里指明了為類型TableInfo生成的XML元素名稱不是對象類型名稱TableInfo,而是Table。
| [System.Xml.Serialization.XmlType("Table")] public class TableInfo |
同樣的方式,我們為類型FieldInfo指定了XML元素名稱為Field,這里展示了特性在C#中的應用。關于特性在未來的某節課程中將講到。
由于能執行XML序列化的屬性必須是可讀寫的,因此在類型FieldInfo中的IsString,IsInteger等屬性為了能執行XML序列化,因此定義了毫無作用的set方法。
XSLT模板說明程序目錄下放置了一些以下劃線開頭的擴展名為XSLT的文件,這就是代碼生成器使用的代碼生成模板。在主界面中使用不同的模板就能生成不同的代碼。在這里我們以_cshaprhashtable.xslt為例子進行說明。
_cshaprhashtable.xslt首先我們在界面中選擇數據表Customers,可以生成它的XML代碼為.
| <Table xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Name>Customers</Name> <Fields> <Field> <Name>Address</Name> <Remark>地址</Remark> <FieldType>Char</FieldType> <IsString>true</IsString> <IsInteger>false</IsInteger> <IsBoolean>false</IsBoolean> <IsNumberic>false</IsNumberic> <IsDateTime>false</IsDateTime> <IsBinary>false</IsBinary> <ValueTypeName>System.String</ValueTypeName> <FieldWidth>60</FieldWidth> <Nullable>true</Nullable> <PrimaryKey>false</PrimaryKey> <Indexed>false</Indexed> </Field> <Field> <Name>City</Name> <Remark>城市</Remark> <FieldType>Char</FieldType> <IsString>true</IsString> <IsInteger>false</IsInteger> <IsBoolean>false</IsBoolean> <IsNumberic>false</IsNumberic> <IsDateTime>false</IsDateTime> <IsBinary>false</IsBinary> <ValueTypeName>System.String</ValueTypeName> <FieldWidth>15</FieldWidth> <Nullable>true</Nullable> <PrimaryKey>false</PrimaryKey> <Indexed>false</Indexed> </Field> <Field>其他字段....</Field> </Fields> </Table> |
而模板_cshaprhashtable.xslt的代碼為
| <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <!-- 根據表結構XML文檔創建影射數據庫字段的 C# 代碼,內部使用 Hashtable 來存儲字段數據 編制 袁永福 2008-1-17 --> <xsl:template match="/*"> <xsl:if test="name(.) != 'Table' "> <font color="red">本模板只能用于單表</font> <br /> </xsl:if> <textarea wrap="off" readonly="1" style=" border:1 solid black; overflow=visible; background-color:#dddddd"> <xsl:variable name="classname"> <xsl:value-of select="concat('DB2_' , Name )" /> </xsl:variable> //***************************************************************************** // 文件名 <xsl:value-of select="Name" />.cs //***************************************************************************** /// <summary> /// 數據庫表 <xsl:value-of select="Name" /> <xsl:if test="Remark!=''"> <xsl:value-of select="concat(' [',Remark,']')" /> </xsl:if> 操作對象 /// </summary> /// <remark> /// 該表有<xsl:value-of select="count(Fields/Field)" />個字段 /// 編制: 代碼生成器 /// 時間: ///</remark> [System.Serializable()] public class <xsl:value-of select="$classname" /> { ///<summary>返回數據表名稱 <xsl:value-of select="Name" /></summary> public static string TableName { get{ return "<xsl:value-of select="Name" />" ; } } ///<summary>返回所有字段的名稱</summary> <xsl:text> public static string[]FieldNames { get { return new string[]{ </xsl:text> <xsl:for-each select="Fields/Field"> <xsl:if test="position()>1"> <xsl:text> ,</xsl:text> </xsl:if> <xsl:text>"</xsl:text> <xsl:value-of select="Name" /> <xsl:text>"</xsl:text> </xsl:for-each> <xsl:text> }; } } #region 定義數據庫字段變量及屬性 ////////////////////////////////////////// private System.Collections.Hashtable myValues = new System.Collections.Hashtable(); </xsl:text> ///<summary>包含所有字段值的列表</summary> <xsl:text> public System.Collections.Hashtable Values { get{ return myValues ;} } </xsl:text> <xsl:for-each select="Fields/Field"><!-- 開始循環遍歷所有的字段定義信息 --> <xsl:variable name="remark"> <xsl:if test="string-length( Remark ) > 0 "> <xsl:value-of select="concat( '(' , Remark , ')')" /> </xsl:if> </xsl:variable> ///<summary>字段值 <xsl:value-of select="Name" /> <xsl:value-of select="$remark" /> <xsl:if test="PrimaryKey='true'">[關鍵字段]</xsl:if> </summary> public <xsl:value-of select="concat( ValueTypeName , ' ' , Name )" /> { get{ return <xsl:choose> <xsl:when test="ValueTypeName='System.Byte[]'"> <xsl:text>( </xsl:text> <xsl:value-of select="ValueTypeName" /> <xsl:text> ) myValues["</xsl:text> <xsl:value-of select="Name" /> <xsl:text>"]</xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>Convert.To</xsl:text> <xsl:value-of select="substring-after( ValueTypeName , '.' )" /> <xsl:text>( myValues["</xsl:text> <xsl:value-of select="Name" /> <xsl:text>"] )</xsl:text> </xsl:otherwise> </xsl:choose> ;} set{ myValues["<xsl:value-of select="Name" />"] = value;} } </xsl:for-each> #endregion }// 數據庫操作類 <xsl:value-of select="$classname" /> 定義結束 </textarea> </xsl:template> </xsl:stylesheet> |
則生成的代碼文本為
| //***************************************************************************** // 文件名 Customers.cs //***************************************************************************** /// <summary> /// 數據庫表 Customers 操作對象 /// </summary> /// <remark> /// 該表有13個字段 /// 編制: 代碼生成器 /// 時間: ///</remark> [System.Serializable()] public class DB2_Customers { ///<summary>返回數據表名稱 Customers</summary> public static string TableName { get{ return "Customers" ; } } ///<summary>返回所有字段的名稱</summary> public static string[] FieldNames { get { return new string[]{ "Address" ,"City" ,"CompanyName" ,"ContactName" ,"ContactTitle" ,"Country" ,"CustomerID" ,"Email" ,"Fax" ,"Phone" ,"PostalCode" ,"Region" ,"WebSite" }; } } #region 定義數據庫字段變量及屬性 ////////////////////////////////////////// private System.Collections.Hashtable myValues = new System.Collections.Hashtable(); ///<summary>包含所有字段值的列表</summary> public System.Collections.Hashtable Values { get{ return myValues ;} } ///<summary>字段值 Address(地址)</summary> public System.String Address { get{ return Convert.ToString( myValues["Address"] ) ;} set{ myValues["Address"] = value;} } -------- 其他字段 --------------------- #endregion }// 數據庫操作類 DB2_Customers 定義結束 |
現在我們根據前因后果來說明其中的過程。在XSLT文件中,唯一的一個xsl:template模板定義塊命中XML文檔的根節點,然后使用name函數來測試當前節點的名稱是否為Table,若不是則輸出“本模板只能用于單表”的提示信息。XSLT沒有return語句,只能按順序執行下去,因此不管XML文檔是否正確還是繼續輸出。
接著輸出textarea標簽,由于使用HTML格式顯示代碼,而這里使用textarea元素,我們就不用做尖括號字符轉義了。
這里有xsl:variable 元素,說明開始定義一個變量,該變量的名稱是classname,也就是這個C#類的名稱。XSLT變量不是真的變量,應當算是常量,其值是不能改變的。這里還用到了concat函數,這是XSLT中字符串連接函數,用于將多個字符串拼湊起來,這個函數的參數個數不固定,但必須等于或超過2個。函數里面可以使用XPath路徑來引用某個XML節點,用單引號來定義固定的字符串。
然后XSLT輸出代碼開頭的一些說明性的注釋文本,這里是用了count函數來累計所有的字段個數。接著開始輸出C#代碼了,在輸出類型定義中使用了xsl:value-of標記來輸出類型名稱,它是用$classname來引用剛才定義的名為classname的XSLT變量。然后我們輸出靜態屬性TableName返回數據表的名稱。
在輸出FieldNames屬性時,我們遍歷輸出了字段名稱,這里有個判斷,若position函數的值大于1則在字段名稱前輸出一個逗號,position函數用于返回當前處理的節點在整個要處理的節點列表中的從1開始的序號。若position值大于1,則表示不是輸出第一個字段名稱,由于這里是定義一個字符串數組的,因此需要添加逗號。
在這里我們使用xsl:text來輸出純文本數據,這里輸出逗號時可以不需要使用xsl:text元素,可以直接輸出,但使用xsl:text元素可以改善XSLT代碼的可讀性。而且xsl:text元素可以輸出連續的空白字符。
接著我們輸出myValues 變量和Values屬性,然后又開始遍歷字段輸出各個字段的屬性代碼了。
首先我們定義了一個名為remark的變量,用于保存字段說明文本。然后輸出字段屬性的說明性注釋,并判斷PrimaryKey元素值,若該元素值為true則輸出文本“關鍵字段”。然后輸出public 屬性數據類型 屬性名稱,這里是用concat函數來連接屬性數據類型和字段名稱,中間加了一個空格。
這里我們使用了xsl:choose結構,xsl:choose類似C#中的swith語句,下面的xsl:when 類似C#的case 語句,而xsl:otherwise 類似C#的default語句。Xsl:choose 可以包含若干個xsl:when,而且最多包含一個xsl:oterwise元素。每一個xsl:when都可以進行各自的判斷,這點和switch不同,更像連續的if else ifelse 語句。
在這里,第一個xsl:when判斷字段類型名稱是否是System.Byte[],也就是字節數組,若是字節數組則輸出字節數組強制轉化的C#代碼。剩余的情況使用xsl:otherwise來輸出使用Convert.To函數進行類型轉換的C#代碼。
經過上述的XSLT轉換處理,我們就能根據描述數據表和字段設計信息的XML文檔來自動生成C#代碼了。把這個C#代碼復制到C#工程中就可以編譯了。這段代碼內部使用一個哈西表來保存字段的值,并使用一個個屬性來影射數據庫表的字段。
在程序目錄下存在一些類似的模板,比如_cshapr.xslt就是生成另外一種的C#代碼的模板,在生成的代碼中不是用哈西列表保存數據,而是是用一個個變量來保存字段數據。_java.xslt是生成JAVA代碼的,_VB6.xslt是生成VB6代碼的。
_HTML.xslt程序目錄下還有其他的代碼生成器模板,例如_HTML.xslt能生成HTML代碼,能是用表格的樣式來展示多個表結構信息。該 模板有個特點就是既能生成單個表的信息,也能生成整個數據庫中所有表的信息。該模板的XSLT代碼為
| <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output indent='yes' /> <xsl:template match="/*"> <xsl:for-each select="http://Table"> <xsl:call-template name="table" /> </xsl:for-each> </xsl:template> <xsl:template name="table"> <div> <span> <span>數據表 </span> <b> <xsl:value-of select="Name" /> </b> <span> 結構,共 </span> <xsl:value-of select="count( Fields/Field )" /> <span> 個字段 </span> <xsl:value-of select="concat(' ', Remark)" /> <xsl:if test="count( Fields/Field[PrimaryKey='true']) > 0 "> <span>關鍵字段:</span> <b> <xsl:for-each select="Fields/Field[PrimaryKey='true']"> <xsl:if test="position()> 1 ">,</xsl:if> <xsl:value-of select="Name" /> </xsl:for-each> </b> </xsl:if> </span> <table style="border: black 1 solid" border="0" width="100%" rules='all' cellspacing="0" cellpadding="0" bordercolor="#000000"> <tr bgcolor="#eeee99"> <td width="150">字段名</td> <td width="80">字段類型</td> <td width="50">長度</td> <td width="80">可否為空</td> <td width="80">是否索引</td> <td>說明</td> </tr> <xsl:for-each select="Fields/Field"> <xsl:sort select="Name" /> <tr valign="top"> <xsl:attribute name="bgcolor"> <xsl:iftest="position() mod 2 = 0">#eeeeee</xsl:if> </xsl:attribute> <td> <font> <xsl:if test="PrimaryKey='true'"> <xsl:attribute name="color">red</xsl:attribute> </xsl:if> <xsl:value-of select="concat(' ' , Name)" /> </font> </td> <td> <xsl:value-of select="FieldType" /> </td> <td> <xsl:value-of select="FieldWidth" /> </td> <td> <xsl:if test="Nullable='true'">是</xsl:if> </td> <td> <xsl:if test="Indexed='true'">是</xsl:if> </td> <td> <xsl:value-of select="Remark" /> <xsl:if test="Description !=''"> <br /> <xsl:value-of select="Description" /> </xsl:if> </td> </tr> </xsl:for-each> </table> </div> <p /> </xsl:template> </xsl:stylesheet> |
現對該模板進行詳細說明。在這個模板中有兩個xsl:template,也就定義了兩個子模板,其中第一個直接使用match命中XML文檔的根節點,而第二個xsl:template創建一個名為table的子模板。按照面向過程的軟件開發思想,第一個模板就是主函數,而第二個模板就定義了一個名為table的函數,主函數內部調用了這個子函數。
在主模板中,首先是用XPath路徑//Table來查找XML文檔中所有的名為Table的節點。在XPath中使用兩個斜線表示查找XML文檔中的當前節點下的所有子孫節點。對于查到的每個Table元素都是用xsl:call-template來調用名為table的子模板。這很類似C語言中主函數調用子函數一樣。
XSLT轉換過程進入的了table的子模板中,首先輸出DIV,輸出一些數據表的說明文字。這里還是用count( Fields/Field[PrimaryKey='true']) > 0 來判斷該表是否有關鍵字段,若有關鍵字段則輸出關鍵字段的名稱。
然后輸出table標簽,輸出第一行表格,然后查找遍歷所有的字段元素,對每一個字段XML元素,首先輸出tr標簽,這里還是用了xsl:attribute元素來設置TR元素的屬性值。這里用了name屬性來指明輸出的是tr元素的名為bgcolor的屬性值,也就是表格行的背景色。里面是用 xsl:if 來輸出該屬性值的內容,這里的判斷條件是 position() mod 2 = 0 ,position()函數返回從1開始的當前節點的序號,mod 表示數學取模運算,因此若當前節點的序號是二的倍數則輸出其內容,也就是#eeeeee ,這是一個淺灰色的顏色值。這樣我們就可以讓表格行每隔一行就設置bgcolor屬性為#eeeeee,實現了表格行的顏色交替變化。
然后我們開始輸出單元格,第一個單元格輸出字段名,這里我們又是用了xsl:attribute,使得若字段為關鍵字段則使用紅色顯示字段名稱。剩下的欄目則原樣輸出。
這樣我們定義了兩個子模板來輸出HTML代碼,能顯示一個或全部的數據表的字段定義信息。
其實我們可以將兩個子模板合并為一個模板,將子模板的XSLT代碼復制到主模板的xsl:for-each里面,也也能用一個模板來輸出一個或全部的數據表的代碼。
類似的在模板_sql.語句.xslt也定義了兩個模板,使得能輸出一個或全部數據表的SQL語句。
從這些例子可以看出,XSLT代碼有點類似面向過程的編程語言。其中有邏輯判斷,循環,條件開關列表,主函數,子函數,可以說是用XML來書寫的程序代碼。XSLT只能根據另外一個XML文件輸出純文本文檔。
運行程序大家獲得程序源代碼后,設置虛擬目錄,并設置安全權限,使得程序能讀取程序目錄下的文件,也不用作其它配置即可運行。部屬完畢后我們就可以在瀏覽器中查看這個代碼生成器的運行結果了。
用戶界面中列出了演示數據庫中所有的數據表名稱,并列出了可用的模板名。大家可以選擇某個數據表名或整個數據庫,然后再選擇某個模板,點擊“創建代碼”按鈕,即可在按鈕下面看到生成的以HTML方式顯示的自動生成的文檔。
大家可以仿造已有的XSLT模板編制自己的代碼生成模板,保存的文件名以下劃線開頭,XSLT為擴展名。放置到程序目錄下,點擊“刷新系統”按鈕就可使用剛剛添加的代碼生成模板。這也算做是一種熱拔插系統了。
本代碼生成器的特點是程序代碼是少量的,功能是強大的,配置是靈活的,移植也是簡單的。由于核心是采用XSLT的,因此很容易翻譯到JAVA,VB.NET,PHP等其它B/S程序。添加和刪除模板的操作也是很簡單的。大家可以根據各自需要,將這個代碼生成器進行一些擴展即可投入實際運用了。
小結在本課程中,我們使用了C#和XSLT技術開發了一個可投入實際運用的代碼生成器了。該代碼生成器結構簡單,功能強大。使我們更深入的發現了XSLT技術的威力。建議大家以后多多學習多多應用.
posted on 2008-05-21 08:55 袁永福 電子病歷,醫療信息化 閱讀(9987) 評論(20) 收藏 舉報
浙公網安備 33010602011771號