.NET Framework中對Attribute的支持是一個全新的功能,這種支持來自它的Attribute類。在你的程序中適當地使用這個類,或者是靈活巧妙地利用這個類,將使你的程序獲得某種在以往編程中很難做到的能力。我們來看一個例子:
假如你是一個項目開發小組中的成員,你想要跟蹤項目代碼檢查的信息,通常你可以把代碼的檢查信息保存在數據庫中以便查詢;或者把信息寫到代碼的注釋里面,這樣可以閱讀代碼的同時看到代碼被檢查的信息。我們知道.NET的組件是自描述的,那么是否可以讓代碼自己來描述它被檢查的信息呢?這樣我們既可以將信息和代碼保存在一起,又可以通過代碼的自我描述得到信息。答案就是使用Attribute.
下面的步驟和代碼告訴你怎么做:
首先,我們創建一個自定義的Attribute,并且事先設定我們的Attribute將施加在class的元素上面以獲取一個類代碼的檢查信息。
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.Class)] //還記得上一節的內容嗎?
public class CodeReviewAttribute : System.Attribute //定義一個CodeReview的Attribute
{
private string reviewer; //代碼檢查人
private string date; //檢查日期
private string comment; //檢查結果信息
//參數構造器
public CodeReviewAttribute(string reviewer, string date)
{
this.reviewer=reviewer;
this.date=date;
}
public string Reviewer
{
get
{
return reviewer;
}
}
public string Date
{
get
{
return date;
}
}
public string Comment
{
get
{
return comment;
}
set
{
comment=value;
}
}
}
用于參數的Attribute
在編寫多層應用程序的時候,你是否為每次要寫大量類似的數據訪問代碼而感到枯燥無味?比如我們需要編寫調用存儲過程的代碼,或者編寫T_SQL代碼,這些代碼往往需要傳遞各種參數,有的參數個數比較多,一不小心還容易寫錯。有沒有一種一勞永逸的方法?當然,你可以使用MS的Data Access Application Block,也可以使用自己編寫的Block。這里向你提供一種另類方法,那就是使用Attribute。
下面的代碼是一個調用AddCustomer存儲過程的常規方法:
public int AddCustomer(SqlConnection connection,
string customerName,
string country,
string province,
string city,
string address,
string telephone)
{
SqlCommand command=new SqlCommand("AddCustomer", connection);
command.CommandType=CommandType.StoredProcedure;
command.Parameters.Add("@CustomerName",SqlDbType.NVarChar,50).Value=customerName;
command.Parameters.Add("@country",SqlDbType.NVarChar,20).Value=country;
command.Parameters.Add("@Province",SqlDbType.NVarChar,20).Value=province;
command.Parameters.Add("@City",SqlDbType.NVarChar,20).Value=city;
command.Parameters.Add("@Address",SqlDbType.NVarChar,60).Value=address;
command.Parameters.Add("@Telephone",SqlDbType.NvarChar,16).Value=telephone;
command.Parameters.Add("@CustomerId",SqlDbType.Int,4).Direction=ParameterDirection.Output;
connection.Open();
command.ExecuteNonQuery();
connection.Close();
int custId=(int)command.Parameters["@CustomerId"].Value;
return custId;
}
上面的代碼,創建一個Command實例,然后添加存儲過程的參數,然后調用ExecuteMonQuery方法執行數據的插入操作,最后返回CustomerId。從代碼可以看到參數的添加是一種重復單調的工作。如果一個項目有100多個甚至幾百個存儲過程,作為開發人員的你會不會要想辦法偷懶?(反正我會的:-))。
下面開始我們的代碼自動生成工程:
我們的目的是根據方法的參數以及方法的名稱,自動生成一個Command對象實例,第一步我們要做的就是創建一個SqlParameterAttribute, 代碼如下:
SqlCommandParameterAttribute.cs
using System;
using System.Data;
using Debug=System.Diagnostics.Debug;
namespace DataAccess
{
// SqlParemeterAttribute 施加到存儲過程參數
[ AttributeUsage(AttributeTargets.Parameter) ]
public class SqlParameterAttribute : Attribute
{
private string name; //參數名稱
private bool paramTypeDefined; //是否參數的類型已經定義
private SqlDbType paramType; //參數類型
private int size; //參數尺寸大小
private byte precision; //參數精度
private byte scale; //參數范圍
private bool directionDefined; //是否定義了參數方向
private ParameterDirection direction; //參數方向
public SqlParameterAttribute()
{
}
public string Name
{
get { return name == null ? string.Empty : name; }
set { _name = value; }
}
public int Size
{
get { return size; }
set { size = value; }
}
public byte Precision
{
get { return precision; }
set { precision = value; }
}
public byte Scale
{
get { return scale; }
set { scale = value; }
}
public ParameterDirection Direction
{
get
{
Debug.Assert(directionDefined);
return direction;
}
set
{
direction = value;
directionDefined = true;
}
}
public SqlDbType SqlDbType
{
get
{
Debug.Assert(paramTypeDefined);
return paramType;
}
set
{
paramType = value;
paramTypeDefined = true;
}
}
public bool IsNameDefined
{
get { return name != null && name.Length != 0; }
}
public bool IsSizeDefined
{
get { return size != 0; }
}
public bool IsTypeDefined
{
get { return paramTypeDefined; }
}
public bool IsDirectionDefined
{
get { return directionDefined; }
}
public bool IsScaleDefined
{
get { return _scale != 0; }
}
public bool IsPrecisionDefined
{
get { return _precision != 0; }
}
...
以上定義了SqlParameterAttribute的字段和相應的屬性,為了方便Attribute的使用,我們重載幾個構造器,不同的重載構造器用于不用的參數: ...
// 重載構造器,如果方法中對應于存儲過程參數名稱不同的話,我們用它來設置存儲過程的名稱
// 其他構造器的目的類似
public SqlParameterAttribute(string name)
{
Name=name;
}
public SqlParameterAttribute(int size)
{
Size=size;
}
public SqlParameterAttribute(SqlDbType paramType)
{
SqlDbType=paramType;
}
public SqlParameterAttribute(string name, SqlDbType paramType)
{
Name = name;
SqlDbType = paramType;
}
public SqlParameterAttribute(SqlDbType paramType, int size)
{
SqlDbType = paramType;
Size = size;
}
public SqlParameterAttribute(string name, int size)
{
Name = name;
Size = size;
}
public SqlParameterAttribute(string name, SqlDbType paramType, int size)
{
Name = name;
SqlDbType = paramType;
Size = size;
}
}
}
為了區分方法中不是存儲過程參數的那些參數,比如SqlConnection,我們也需要定義一個非存儲過程參數的Attribute:
//NonCommandParameterAttribute.cs
using System;
namespace DataAccess
{
[ AttributeUsage(AttributeTargets.Parameter) ]
public sealed class NonCommandParameterAttribute : Attribute
{
}
}
我們已經完成了SQL的參數Attribute的定義,在創建Command對象生成器之前,讓我們考慮這樣的一個事實,那就是如果我們數據訪問層調用的不是存儲過程,也就是說Command的CommandType不是存儲過程,而是帶有參數的SQL語句,我們想讓我們的方法一樣可以適合這種情況,同樣我們仍然可以使用Attribute,定義一個用于方法的Attribute來表明該方法中的生成的Command的CommandType是存儲過程還是SQL文本,下面是新定義的Attribute的代碼:
//SqlCommandMethodAttribute.cs
using System;
using System.Data;
namespace Emisonline.DataAccess
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class SqlCommandMethodAttribute : Attribute
{
private string commandText;
private CommandType commandType;
public SqlCommandMethodAttribute( CommandType commandType, string commandText)
{
commandType=commandType;
commandText=commandText;
}
public SqlCommandMethodAttribute(CommandType commandType) : this(commandType, null){}
public string CommandText
{
get
{
return commandText==null ? string.Empty : commandText;
}
set
{
commandText=value;
}
}
public CommandType CommandType
{
get
{
return commandType;
}
set
{
commandType=value;
}
}
}
}
我們的Attribute的定義工作已經全部完成,下一步就是要創建一個用來生成Command對象的類。
SqlCommandGenerator類的設計
SqlCommandGEnerator類的設計思路就是通過反射得到方法的參數,使用被SqlCommandParameterAttribute標記的參數來裝配一個Command實例。
引用的命名空間:
//SqlCommandGenerator.cs
using System;
using System.Reflection;
using System.Data;
using System.Data.SqlClient;
using Debug = System.Diagnostics.Debug;
using StackTrace = System.Diagnostics.StackTrace;
類代碼:
namespace DataAccess
{
public sealed class SqlCommandGenerator
{
//私有構造器,不允許使用無參數的構造器構造一個實例
private SqlCommandGenerator()
{
throw new NotSupportedException();
}
//靜態只讀字段,定義用于返回值的參數名稱
public static readonly string ReturnValueParameterName = "RETURN_VALUE";
//靜態只讀字段,用于不帶參數的存儲過程
public static readonly object[] NoValues = new object[] {};
public static SqlCommand GenerateCommand(SqlConnection connection,
MethodInfo method, object[] values)
{
//如果沒有指定方法名稱,從堆棧幀得到方法名稱
if (method == null)
method = (MethodInfo) (new StackTrace().GetFrame(1).GetMethod());
// 獲取方法傳進來的SqlCommandMethodAttribute
// 為了使用該方法來生成一個Command對象,要求有這個Attribute。
SqlCommandMethodAttribute commandAttribute =
(SqlCommandMethodAttribute) Attribute.GetCustomAttribute(method, typeof(SqlCommandMethodAttribute));
Debug.Assert(commandAttribute != null);
Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure ||
commandAttribute.CommandType == CommandType.Text);
// 創建一個SqlCommand對象,同時通過指定的attribute對它進行配置。
SqlCommand command = new SqlCommand();
command.Connection = connection;
command.CommandType = commandAttribute.CommandType;
// 獲取command的文本,如果沒有指定,那么使用方法的名稱作為存儲過程名稱
if (commandAttribute.CommandText.Length == 0)
{
Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure);
command.CommandText = method.Name;
}
else
{
command.CommandText = commandAttribute.CommandText;
}
// 調用GeneratorCommandParameters方法,生成command參數,同時添加一個返回值參數
GenerateCommandParameters(command, method, values);
command.Parameters.Add(ReturnValueParameterName, SqlDbType.Int).Direction
=ParameterDirection.ReturnValue;
return command;
}
private static void GenerateCommandParameters(
SqlCommand command, MethodInfo method, object[] values)
{
// 得到所有的參數,通過循環一一進行處理。
ParameterInfo[] methodParameters = method.GetParameters();
int paramIndex = 0;
foreach (ParameterInfo paramInfo in methodParameters)
{
// 忽略掉參數被標記為[NonCommandParameter ]的參數
if (Attribute.IsDefined(paramInfo, typeof(NonCommandParameterAttribute)))
continue;
// 獲取參數的SqlParameter attribute,如果沒有指定,那么就創建一個并使用它的缺省設置。
SqlParameterAttribute paramAttribute = (SqlParameterAttribute) Attribute.GetCustomAttribute(
paramInfo, typeof(SqlParameterAttribute));
if (paramAttribute == null)
paramAttribute = new SqlParameterAttribute();
//使用attribute的設置來配置一個參數對象。使用那些已經定義的參數值。如果沒有定義,那么就從方法
// 的參數來推斷它的參數值。
SqlParameter sqlParameter = new SqlParameter();
if (paramAttribute.IsNameDefined)
sqlParameter.ParameterName = paramAttribute.Name;
else
sqlParameter.ParameterName = paramInfo.Name;
if (!sqlParameter.ParameterName.StartsWith("@"))
sqlParameter.ParameterName = "@" + sqlParameter.ParameterName;
if (paramAttribute.IsTypeDefined)
sqlParameter.SqlDbType = paramAttribute.SqlDbType;
if (paramAttribute.IsSizeDefined)
sqlParameter.Size = paramAttribute.Size;
if (paramAttribute.IsScaleDefined)
sqlParameter.Scale = paramAttribute.Scale;
if (paramAttribute.IsPrecisionDefined)
sqlParameter.Precision = paramAttribute.Precision;
if (paramAttribute.IsDirectionDefined)
{
sqlParameter.Direction = paramAttribute.Direction;
}
else
{
if (paramInfo.ParameterType.IsByRef)
{
sqlParameter.Direction = paramInfo.IsOut ?
ParameterDirection.Output :
ParameterDirection.InputOutput;
}
else
{
sqlParameter.Direction = ParameterDirection.Input;
}
}
// 檢測是否提供的足夠的參數對象值
Debug.Assert(paramIndex < values.Length);
//把相應的對象值賦于參數。
sqlParameter.Value = values[paramIndex];
command.Parameters.Add(sqlParameter);
paramIndex++;
}
//檢測是否有多余的參數對象值
Debug.Assert(paramIndex == values.Length);
}
}
}
必要的工作終于完成了。SqlCommandGenerator中的代碼都加上了注釋,所以并不難讀懂。下面我們進入最后的一步,那就是使用新的方法來實現上一節我們一開始顯示個那個AddCustomer的方法。
重構新的AddCustomer代碼:
[ SqlCommandMethod(CommandType.StoredProcedure) ]
public void AddCustomer( [NonCommandParameter] SqlConnection connection,
[SqlParameter(50)] string customerName,
[SqlParameter(20)] string country,
[SqlParameter(20)] string province,
[SqlParameter(20)] string city,
[SqlParameter(60)] string address,
[SqlParameter(16)] string telephone,
out int customerId )
{
customerId=0; //需要初始化輸出參數
//調用Command生成器生成SqlCommand實例
SqlCommand command = SqlCommandGenerator.GenerateCommand( connection, null, new object[]
{customerName,country,province,city,address,telephone,customerId } );
connection.Open();
command.ExecuteNonQuery();
connection.Close();
//必須明確返回輸出參數的值
customerId=(int)command.Parameters["@CustomerId"].Value;
}
代碼中必須注意的就是out參數,需要事先進行初始化,并在Command執行操作以后,把參數值傳回給它。受益于Attribute,使我們擺脫了那種編寫大量枯燥代碼編程生涯。 我們甚至還可以使用Sql存儲過程來編寫生成整個方法的代碼,如果那樣做的話,可就大大節省了你的時間了,上一節和這一節中所示的代碼,你可以把它們單獨編譯成一個組件,這樣就可以在你的項目中不斷的重用它們了。從下一節開始,我們將更深層次的介紹Attribute的應用,請繼續關注。
Attribute在攔截機制上的應用
從這一節開始我們討論Attribute的高級應用,為此我準備了一個實際的例子:我們有一個訂單處理系統,當一份訂單提交的時候,系統檢查庫存,如果庫存存量滿足訂單的數量,系統記錄訂單處理記錄,然后更新庫存,如果庫存存量低于訂單的數量,系統做相應的記錄,同時向庫存管理員發送郵件。為了方便演示,我們對例子進行了簡化:
//Inventory.cs
using System;
using System.Collections;
namespace NiwalkerDemo
{
public class Inventory
{
private Hashtable inventory=new Hashtable();
public Inventory()
{
inventory["Item1"]=100;
inventory["Item2"]=200;
}
public bool Checkout(string product, int quantity)
{
int qty=GetQuantity(product);
return qty>=quantity;
}
public int GetQuantity(string product)
{
int qty=0;
if(inventory[product]!=null)
qty = (int)inventory[product];
return qty;
}
public void Update(string product, int quantity)
{
int qty=GetQuantity(product);
inventory[product]=qty-quantity;
}
}
}
//Logbook.cs
using System;
namespace NiwalkerDemo
{
public class Logbook
{
public static void Log(string logData)
{
Console.WriteLine("log:{0}",logData);
}
}
}
//Order.cs
using System;
namespace NiwalkerDemo
{
public class Order
{
private int orderId;
private string product;
private int quantity;
public Order(int orderId)
{
this.orderId=orderId;
}
public void Submit()
{
Inventory inventory=new Inventory(); //創建庫存對象
//檢查庫存
if(inventory.Checkout(product,quantity))
{
Logbook.Log("Order"+orderId+" available");
inventory.Update(product,quantity);
}
else
{
Logbook.Log("Order"+orderId+" unavailable");
SendEmail();
}
}
public string ProductName
{
get{ return product; }
set{ product=value; }
}
public int OrderId
{
get{ return orderId; }
}
public int Quantity
{
get{ return quantity;}
set{ quantity=value; }
}
public void SendEmail()
{
Console.WriteLine("Send email to manager");
}
}
}
下面是調用程序:
//AppMain.cs
using System;
namespace NiwalkerDemo
{
public class AppMain
{
static void Main()
{
Order order1=new Order(100);
order1.ProductName="Item1";
order1.Quantity=150;
order1.Submit();
Order order2=new Order(101);
order2.ProductName="Item2";
order2.Quantity=150;
order2.Submit();
}
}
}
程序看上去還不錯,商務對象封裝了商務規則,運行的結果也符合要求。但是我好像聽到你在抱怨了,沒有嗎?當你的客戶的需求改變的時候(客戶總是經常改變他們的需求),比如庫存檢查的規則不是單一的檢查產品的數量,還要檢查產品是否被預訂的多種情況,那么你需要改變Inventory的代碼,同時還要修改Order中的代碼,我們的例子只是一個簡單的商務邏輯,實際的情況比這個要復雜的多。問題在于Order對象同其他的對象之間是緊耦合的,從OOP的觀點出發,這樣的設計是有問題的,如果你寫出這樣的程序,至少不會在我的團隊里面被Pass.
你說了:“No problem! 我們可以把商務邏輯抽出來放到一個專門設計的用來處理事務的對象中!编,好主意,如果你是這么想的,或許我還可以給你一個提議,使用Observer Design Pattern(觀察者設計模式):你可以使用delegate,在Order對象中定義一個BeforeSubmit和AfterSubmit事件,然后創建一個對象鏈表,將相關的對象插入到這個鏈表中,這樣就可以實現對Order提交事件的攔截,在Order提交之前和提交之后自動進行必要的事務處理。如果你感興趣的話,你可以自己動手來編寫這樣的一個代碼,或許還要考慮在分布式環境中(Order和Inventory不在一個地方)如何處理對象之間的交互問題。
幸運的是,.NET Framework中提供了實現這種技術的支持。在.NET Framework中的對象Remoting和組件服務中,有一個重要的攔截機制,在對象Remoting中,不同的應用程序之間的對象的交互需要穿越他們的域邊界,每一個應用域也可以細分為多個Context(上下文環境),每一個應用域也至少有一個默認的Context,即使在同一個應用域,也存在穿越不同Context的問題。NET的組件服務發展了COM+的組件服務,它使用Context Attribute來實現象COM+一樣的攔截功能。通過對調用對象的攔截,我們可以對一個方法的調用進行前處理和后處理,同時也解決了上述的跨越邊界的問題。
需要提醒你,如果你在MSDN文檔查ContextAttribute,我可以保證你得不到任何有助于了解ContextAttribute的資料,你看到的將是這么一句話:“This type supports the .NET Framework infrastructure and is not intended to be used directly from your code.”——“本類型支持.NET Framework基礎結構,它不打算直接用于你的代碼!辈贿^,在msdn站點,你可以看到一些有關這方面的資料(見文章后面的參考鏈接)。
下面我們介紹有關的幾個類和一些概念,首先是:
ContextAttribute類
ContextAttribute派生自Attribute,同時它還實現了IContextAttribute和IContextProperty接口。所有自定義的ContextAttribute必須從這個類派生。
構造器:
ContextAttribute:構造器帶有一個參數,用來設置ContextAttribute的名稱。
公共屬性:
Name:只讀屬性。返回ContextAttribute的名稱
公共方法:
GetPropertiesForNewContext:虛擬方法。向新的Context添加屬性集合。
IsContextOK:虛擬方法。查詢客戶Context中是否存在指定的屬性。
IsNewContextOK:虛擬方法。默認返回true。一個對象可能存在多個Context,使用這個方法來檢查新的Context中屬性是否存在沖突。
Freeze:虛擬方法。該方法用來定位被創建的Context的最后位置。
ContextBoundObject類
實現被攔截的類,需要從ContextBoundObject類派生,這個類的對象通過Attribute來指定它所在Context,凡是進入該Context的調用都可以被攔截。該類從MarshalByRefObject派生。
以下是涉及到的接口:
IMessage:定義了被傳送的消息的實現。一個消息必須實現這個接口。
IMessageSink:定義了消息接收器的接口,一個消息接收器必須實現這個接口。
還有幾個接口,我們將在下一節結合攔截構架的實現原理中進行介紹。
我們的自定義CodeReviewAttribute同普通的類沒有區別,它從Attribute派生,同時通過AttributeUsage表示我們的Attribute僅可以施加到類元素上。
第二步就是使用我們的CodeReviewAttribute, 假如我們有一個Jack寫的類MyClass,檢查人Niwalker,檢查日期2003年7月9日,于是我們施加Attribute如下:
[CodeReview("Niwalker","2003-7-9",Comment="Jack的代碼")]
public class MyClass
{
//類的成員定義
}
當這段代碼被編譯的時候,編譯器會調用CodeReviewAttribute的構造器并且把"Niwalker"和"2003-7-9"分別作為構造器的參數。注意到參數表中還有一個Comment屬性的賦值,這是Attribute特有的方式,這里你可以設置更多的Attribute的公共屬性(如果有的話),需要指出的是.NET Framework1.0允許向private的屬性賦值,但在.NET Framework1.1已經不允許這樣做,只能向public的屬性賦值。
第三步就是取出我們需要的信息,這是通過.NET的反射來實現的,關于反射的知識,限于篇幅我不打算在這里進行說明,也許我會在以后另外寫一篇介紹反射的文章。
class test
{
static void Main(string[] args)
{
System.Reflection.MemberInfo info=typeof(MyClass); //通過反射得到MyClass類的信息
//得到施加在MyClass類上的定制Attribute
CodeReviewAttribute att=
(CodeReviewAttribute)Attribute.GetCustomAttribute(info,typeof(CodeReviewAttribute));
if(att!=null)
{
Console.WriteLine("代碼檢查人:{0}",att.Reviewer);
Console.WriteLine("檢查時間:{0}",att.Date);
Console.WriteLine("注釋:{0}",att.Comment);
}
}
}
在上面這個例子中,Attribute扮演著向一個類添加額外信息的角色,它并不影響MyClass類的行為。通過這個例子,我們大致可以知道如何寫一個自定義的Attribute,以及如何在應用程序使用它。下一節,我將介紹如何使用Attribute來自動生成ADO.NET的數據訪問類的代碼。