这里是示例代码
利用反射解决QueryString和Session中的参数绑定问题
一. 前言
本文主要译自网上的一篇文章(http://www.codeproject.com/aspnet/WebParameter.asp),自己又作了一些改进,因此形成了这篇心得。
二. 简介:
本文介绍了在ASP.NET页面对象中利用元数据自动从Page.Request对象的QueryString中填充页面的变量和属性的策略.
三. 问题的提出
当你对ASP.NET页面进行参数化时,比如:从Request.QueryString(GET)或Request.Form(POST)中获取参数,你需要花大量的时间写类似以下的代码:
{
get
{
return Request.QueryString["FirstName"];
}
}
但是你必须处理潜在的FirstName为null值是的情况,因此你可能会通过如下的代码提供一个缺省值
return test==null ? defaultValue : test;
}
protected string FirstName_NullSafe{
get{ return IsNull(Request.QueryString["FirstName"],""); }
}
对于非字符串类型,你也可以通过稍微修改以上代码来决定当参数为null时是提供一个缺省值还是抛出一个异常,只是要用到Convert.ToXXX()函数
get {
object o=Request.QueryString["CustomerID"];
if (o==null)//如果为空,则抛出异常
{
throw new ApplicationException("Customer ID is required to be passed");
}
else
{
try {
//如果不空,转换成Int32型
return Convert.ToInt32(o,10);
}
catch(Exception err) {
throw new ApplicationException("Invalid CustomerID", err);
}
}
}
}
因此,在Page_Load函数里你可能会写如下的代码进行参数初始化
string o;
// 提供一个初始值
FirstName2 =IsNull(Request.QueryString["FirstName"], "");
//确保该值不为空,否则抛出异常
o =Request.QueryString["CustomerID"];
if (o==null)
throw new ApplicationException("Customer ID is required to be passed");
else
try{//如果转换类型不正常,抛出异常
CustomerID2 = Convert.ToInt32(o,10);
}
catch(Exception err){
throw new ApplicationException("Invalid CustomerID", err);
}
}
小结:
当你在处理从Request.QueryString或Request.Form读取参数时,有以下的固定模式
- 从
Request.QueryString
(或Request.Form
)中检索值, - 当检索的值为空时抛出异常或提供一个缺省值
- 将检索到的值转换成合适的数据成员(field)或属性(property)
- 如果检索值不能正确转换类型,抛出一个异常或提供一个缺省值
四. 解决方案:声明性参数绑定
l 表现形式:
一个解决的方案是使用元数据(metadata),并让一些函数执行实际的工作
比如说以下形式:
protected string FirstName;
//如果没有提供参数,则表示查找和FirstName同名的关键字,
//即QueryString[“FirstName”],
[WebParameter("Last_Name")]
protected string LastName;
//表示从QueryString中查找"Last_Name"的变量值
//即QueryString[“Last_Name”]
[WebParameter(IsRequired=true)]
protected int CustomerID;
//表示如果QueryString[“CustomerID”]返回空值,则抛出异常
可选的构造函数提供了从Request.QueryString
中查找参数的关键字,IsRequired
属性决定了当该参数缺失时是否抛出异常。
l 思路
本解决方案的思路是,利用反射来检索到附加在变量上的元数据,然后根据该元数据的属性决定如何对变量进行存取,其存取模式采用如第2节(《问题的提出》)最后一部分小结里的模式,只是改成了由程序自动判断进行。
1、 检索元数据属性
如前所述,当先定义一个从System.Attribute派生的元数据类WebParameterAttribut,
public class WebParameterAttribute : Attribute
{
public int ParamName;
public string DefaultValue
{
get{ return _default ; }
set{ _default=value ; }
}
}
说明:
protected string LastName;
当以上面形式使用自定义的属性类时,实际上等与是生成了一个和LastName的类型对象LastName.GetType()相关联的WebParameterAttribute类的对象,通过LastName.GetType()对象可以检索到WebParameter对象的一些方法成员、数据成员以及属性等;
我们可以定义WebParameterAttribute类的静态方法,WebParameterAttribute.SetValues(object target, System.Web.HttpRequest request)完成自动初始化某个页面的所有参数,该方法有两个参数:target表示将被自动进行参数化的对象,例如可以是某个页面对象(Page);request是在某个页面对象的初始化时传来的Request对象;后面我们会详细解释SetValues方法的设计
示例如下面粗体部分所示:
{
[WebParameter("txtFirstName")]
public string FirstName ="field default";
[WebParameter("txtLastName", DefaultValue="attribute default")]
public string LastName{
get{ return (string)ViewState["LastName"]; }
set{ ViewState["LastName"]=value; }
}
[WebParameter ("txtCustomerID", DefaultValue="0")]
public int CustomerID;
int TestNum;//注意该数据成员没有关联WebParameter属性
protected System.Web.UI.HtmlControls.HtmlInputText txtFirstName;
protected System.Web.UI.HtmlControls.HtmlInputText txtLastName;
protected System.Web.UI.HtmlControls.HtmlInputText txtCustomerID;
private void Page_Load(object sender, System.EventArgs e)
{
WebParameterAttribute.SetValues(this, Request);
//注意这里是用法
txtFirstName.Value =FirstName;
txtLastName.Value =LastName;
txtCustomerID.Value =CustomerID.ToString();
}
在上面的示例中,静态方法WebParameterAttribut.SetValues先获取this指针代表的Page对象的类型对象this.GetType(),然后用该类型对象获取Page对象的所有变量和属性的信息,即可以获得并操纵Page对象的FirstName,LastName和CustomerID,也包括TestNum。然后SetValues方法检查每个检索到的变量和属性,看有没有关联WebParameter对象,相对来说,数据成员TestNum就没有关联WebParameter对象。
当SetValues检测到变量(field)或属性(property)关联有WebParameter对象时,该方法会检索和变量或属性向关联的WebParameter对象的具体属性,比如说:
protected string LastName;
生成了一个WebParameter对象,并传递给构造函数一个参数”Last_Name”,SetValues方法会检测出和string LastName相关联的WebParameter中包含的“Last_Name”并将其作为要在QueryString中检测的变量的名字。
l 代码解析
下面是SetValues的具体实现:
² 变量及属性定义
public class WebParameterAttribute : Attribute
{
定义私有变量
Constructors
public string ParameterName
{
get{ return _name; }
set{ _name=value; }
}
public string DefaultValue
{
get{ return _default ; }
set{ _default=value ; }
}
public bool IsRequired
{
get{ return _isRequired; }
set{ _isRequired=value; }
}
/// </summary>
public bool IsDefaultUsedForInvalid
{
get{ return _isDefaultForInvalid; }
set{ _isDefaultForInvalid=value; }
}
² SetValues方法
从request中检索特定值并设定target中指定变量和属性的值
{//传入的target对象可以是Page对象,request可以是Page.Request对象
//获取target的类型信息
System.Type type =target.GetType();
//获取target公开的、实例化后的成员信息
MemberInfo[] members= type.GetMembers(BindingFlags.Instance |
BindingFlags.Public|
BindingFlags.NonPublic);
//Instance:只对非静态的成员查找;
//Public:只对具有公开访问属性的成员进行查找
//在和Instance或static联用时,
//必须指定NonPublic或Public,否则返回空值
foreach(MemberInfo member in members)
{
if((member is FieldInfo) || (member is PropertyInfo))
{//对变量和属性,设置其初始值
SetValue(member, target, request);
}
}
}
² SetValue方法
根据member对象的类型是变量(Field)或属性(Property),从request中检索特定值并设置target.member的值
{
WebParameterAttribute[] attribs;
WebParameterAttribute attrib;
TypeConverter converter;
string paramValue;
string paramName;
object typedValue;
bool usingDefault;
try
{
attribs=(WebParameterAttribute[])member.GetCustomAttributes(typeof(WebParameterAttribute), true);
//返回和变量或属性关联的WebAttribute对象或其子类的对象
if(attribs!=null && attribs.Length>0)
{
//对于索引属性,不支持,所谓索引属性,
//是指C#中可以用类似数组下标形式访问的属性
if (member.MemberType==MemberTypes.Property)
{
ParameterInfo[] ps= ((PropertyInfo)member).GetIndexParameters();
if (ps!=null && ps.Length>0)
throw new NotSupportedException("Cannot apply WebParameterAttribute to indexed property");
}
//因为WebAttribute在每个成员上只能应用一次,
//所以取第一个就行了
attrib=attribs[0];
//获取[WebParameter(“Last_Name”)]中的”Last_Name”字符串
//如果是类似[WebParameter()] string Name;形式,则
//得到的名字是”Name”,和变量或属性的名字同名
paramName=(attrib.ParameterName!=null)? attrib.ParameterName : member.Name;
//下面将获取paramValue值,但是是string类型
//WebParameter自定义方法GetValue,后面有说明
//从request对象获取QueryString[paramName]值
//或是Form[paramName]值
paramValue=attrib.GetValue(paramName, request);
//如果在QueryString中未检索到paramName所对应的值
usingDefault =false;
if (paramValue==null)
{
if (attrib.DefaultValue!=null)
{//如果设定了缺省值,则用缺省值定赋值
paramValue =attrib.DefaultValue;
usingDefault =true;
}
else if (!attrib.IsRequired)
{//如果没有定义缺省值,且不要求一定有值,返回
return; // 仅仅跳过该值
}
else
{//如果要求必须有值,且未检索到值,则抛出异常
throw new ApplicationException(
String.Format(
"Missing required parameter '{0}'",
paramName)
);
}
//下面将string类型的paramValue
//根据变量或属性的类型作转换,存放在typedValue中
//对变量或属性,类型转换稍有不同
//根据成员是变量(FieldType),属性(PropertyType)
//两种情况获取member的Type类型对象,
//如果不是变量或属性,则抛出异常
//注意:FiledInfo和PropertyInfo,MethodInfo
//都是MemberInfo的子类
//获取成员对应的类型转换器
converter=TypeDescriptor.GetConverter(
GetMemberUnderlyingType(member));
if(converter==null || !converter.CanConvertFrom(paramValue.GetType()))
{//如果转换器为空或不能从字符串类型转换,抛出异常
throw new ApplicationException(
String.Format(
"Could not convert from {0}",
paramValue.GetType()
)
);
}
try
{//将paramValue从字符串类型转换成
//对应成员变量或属性的类型
typedValue=converter.ConvertFrom(paramValue);
SetMemberValue(member, target, typedValue);
//注:自定义SetMemberValue,将target.member的值
//设为同类型的typedValue;
}
catch
{
// 这里捕获类型转换和在设置变量或属性中的异常错误
//如果定义了缺省值,且确定当发生无效值时使用缺省值
//设定变量或属性为缺省值
if (!usingDefault && attrib.IsDefaultUsedForInvalid && attrib.DefaultValue!=null)
{
typedValue =converter.ConvertFrom( attrib.DefaultValue);
SetMemberValue(member, target, typedValue);
}
else
throw;
}
}
}
catch(Exception err)
{
throw new ApplicationException( "Property/field {0} could not be set from request - " + err.Message,err);
}
}
² GetValue方法
从request中检索名paramName对应的值,返回字符串
{//该函数可以重载,以支持Session,QueryString,Form,ViewState类型
if (request.HttpMethod.ToLower()=="post")
return request.Form[paramName];
else
return request.QueryString[paramName];
}
² GetMemberUnderlyingType方法
获取变量或属性对应的类型变量
{
//获取member的Type类型对象,如果不是变量或属性,则抛出异常
//注意:FiledInfo和PropertyInfo,MethodInfo都是MemberInfo的子类
switch(member.MemberType)
{
case MemberTypes.Field:
return ((FieldInfo)member).FieldType;
case MemberTypes.Property:
return ((PropertyInfo)member).PropertyType;
default:
throw new ArgumentException(
"Expected a FieldInfo or PropertyInfo",
"member");
}
² SetMemberValue方法
根据变量或属性两种情况对成员进行赋值
{//对变量(Field)和属性(Property)的SetValue方法不太一样,
//所以要分成两种情况考虑
switch(member.MemberType)
{
case MemberTypes.Field:
((FieldInfo)member).SetValue(target, value);
break;
case MemberTypes.Property:
// 我们已经知道该属性不是索引属性,
//所以该属性不会是数组,设置序号为0的属性就可以了
((PropertyInfo)member).SetValue(
target,
value,
new Object[0]//对序号为0的属性设置
);
break;
}
}
五. 改进
为了实际解决QueryString和Session的参数化问题,对WebParameterAttribute类做了些修改,同时以WebParameterAttribute类为基类派生了QueryParameterAttribute类和SessionParameterAttribute类,分别负责QueryString和Session的参数化。
具体做法如下:
在WebParameterAttribute类中添加了两类静态函数,一类是用于从QueryString,Session,ViewState中读取数据到Page类的成员变量中去。相应的实现函数分别是SetValuesFromBuffer,SetValueFromBuffer,SetMemberValue;另一类用于将Page类的成员变量值写入到Session,ViewState的变量中去。相应的实现函数分别示SetValuesToBuffer,SetValueToBuffer,SetBufferValue。这两类函数的具体实现请看程序源代码,原理和前面讲述的一样。
六. 有待改进的地方
以参数BindingFlags.NonPublic调用Type.GetMembers方法时,系统规定反射代码只能存取被反射对象的protected及public访问类型的成员,要访问private成员,需要用到ReflectionPermission类赋于代码一定权限,而MSDN和其他的资料里关于如何使用ReflectionPermission类访问被反射对象的private成员语焉不详。所以目前本解决方案中还访问不到Page对象的private成员,而只能访问protected和public成员。
七. 技术文档
l AttributeUsageAttribute类
形如
[MyTest(value,attr1=value1,attr2=value2)]
的代码实际上是用类MyTestAttribute定义了一个对象MyTestAttribute(value),而value则是传递的构造函数的参数,可以不止一个,但是要按照定义构造函数时参数的顺序传递。而形如attr1=value1,attr2=value2,则是在MyTestAttribute类中定义了attr1,attr2属性(property),可以在生成MyTestAttribute的对象时给这些属性赋值,这是可选的。
因此,前面的代码表明了
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
实际上是AttributeUsageAttribute类生成的一个对象,传递给构造函数的参数为AttributeTargets.Field | AttributeTargets.Property,同时给两个命名属性赋了值。
关于AttributeUsageAttribute,MSDN里的说明是:定义您自己的特性类时,可通过在特性类上放置 AttributeUsageAttribute 来控制特性类的使用方式。指示的特性类必须直接或间接地从 Attribute 派生。
换句话说,我们定义一个自己的特性类WebParameterAttribute类时,要从Attribute派生,同时用AttributeUsageAttribute的对象来控制自己的WebParameterAttribute类将来可以怎样被使用。
向构造函数传递的参数为AttributeTargets类型的枚举变量,有以下几种组合:
Class:可以将自定义的特性类用于一个类
例:
public class ClassTargetAttribute : Attribute
{
}
[ClassTarget]
public class TestClass
{
}
//错误的用法
[ClassTarget]
public int CustomerID;
//因为ClassTargetAttribute由
//[AttributeUsage(AttributeTargets.Class)]表明了只能用于类
Field:可以将自定义的特性类用于一个成员变量
例:
public class FieldTargetAttribute : Attribute
{
}
[FieldTarget]
public int Count;
Property:可以将自定义的特性类用于一个属性
例:
[AttributeUsage(AttributeTargets.Property)]
public class PropertyTargetAttribute : Attribute
{
}
[PropertyTarget]
public string Name
{
get{return m_name;}
set{m_name=value;}
}
AttributeUsageAttribute的两个命名参数AllowMultiple=false, Inherited=true,其中AllowMultiple是指是否允许在同一个对象上多次使用自定义的属性类,而Inherited指明该属性是否可以有派生类:
例如:
[AttributeUsage(AttributeTargets.Property,AllowMultiple=false)]
public class PropertyTargetAttribute(string Name) : Attribute
{
}
//错误的用法,因为AllowMultiple指明了
//不允许在同一对象上多次使用该属性
[PropertyTarget(“John”)]
[PropertyTarget(“Tom”) ]
class TestClass
{
}
To:SetValues
当在派生类中重写时,使用指定绑定约束,搜索为当前 Type 定义的成员。
public abstract MemberInfo[] GetMembers(
BindingFlags bindingAttr
);
参数
可以使用下列 BindingFlags 筛选标志定义包含在搜索中的字段:
为了获取返回值,必须指定 BindingFlags.Instance 或 BindingFlags.Static。
指定 BindingFlags.Public 可在搜索中包含公共字段。
指定 BindingFlags.NonPublic 可在搜索中包含非公共字段(即私有字段和受保护的字段)。
指定 BindingFlags.FlattenHierarchy 可包含层次结构上的静态成员。
下列 BindingFlags 修饰符标志可用于更改搜索的执行方式:
BindingFlags.DeclaredOnly,表示仅搜索在 Type 上声明的字段,
而不搜索简单继承的字段。
注意 :
必须与 Public 或 NonPublic 一起指定 Instance 或 Static,否则将不返回成员。
有关更多信息,请参见 System.Reflection.BindingFlags。
2、 MemberInfo. GetCustomAttributes
To:SetValue
在派生类中被重写时,返回由 Type 标识的自定义特性的数组。
public abstract object[] GetCustomAttributes(
Type attributeType,
bool inherit
);
参数
attributeType
要搜索的属性类型。只返回可分配给此类型的属性。
inherit
指定是否搜索该成员的继承链以查找这些属性。