使用Attribute简单地扩展WebForm
背景
WebForm的封装性很强,这一方面有利于面向构件的设计和应用,另一方面又使得扩展变得困难,此文将通过2个典型的例子来展示对WebForm的扩展,同时又不使用一个页面基类,仅仅通过外部方法对Page进行扩展。
第一点,对页面流程的限制
很多时候,我们要对页面的进入条件进行限制,比如以下地址
http://www.mywebsite.com/ViewPost.aspx?ID=3
这个地址需要在QueryString中带有键值为ID的值,如果没有此值,页面的执行将产生错误,这通常是一个NullReferenceException。
为了检测QueryString中是否存在ID,我们往往在Page_Load中写以下代码
{
//处理错误
}
虽然代码本身并不负责,但是在十几个页面中连续地这么写是令人头疼的一件事,因此我们需要一种简单的方案,在我的方案中,我们在类上加上一个特定的Attribute即可,其代码如下
public partial class Default : System.Web.UI.Page
{
//其他内容
}
显然这种声明式地编程相比之前的解决方案是令人愉快的,也使开发效率大大提高
第二点,对属性的自动注值
在以前例说明,当QueryString中确实存在ID的值的时候,我们就要取得此ID的值,因此我们往往会在Page_Load中写如下代码
//这里假设idStr是存在的
long id = default(long);
bool isParsed = Int32.TryParse(idStr, out id);
if (isParsed == false)
{
//处理错误
}
//使用id进行处理
显然这样的代码写十几次也是另人讨厌的,因此我们继续换一种方案,这次我们在属性上加特定的Attribute,结果如下
public int ID
{
private get;
set;
}
随后在Page_Load中可以直接使用ID,这同样令人愉快,也同样提高了开发速度
页面拦截功能实现方案
1.制作对页面的拦截器
首先定义一个IInterceptor接口,其方法可以对页面的Load事件进行拦截,在前后进行相应的处理
{
void ExecutePreLoad(Page page);
void ExecutePostLoad(Page page);
}
2.实现接口
有了接口,就到了自由发挥的时候了,以上面的检查QueryString为例,设计一个QueryStringCheckInterceptor
方法的实现并不难,但是考虑到我们的Interceptor并不能真正地介入页面的流程控制其流程的停止与执行,为了向系统告知此页面因为不满足条件而产生错误,采用了异常的方式,在页面的QureyString不满足条件的情况下,抛出一个自定义的PageNotApplicableException即可
3.把拦截器加到页面上
这时自然要用到Attribute了,首先自定义一个Attribute,称之为InterceptorAttribute
当然还要示能从Attribute上取得拦截器以进行相关逻辑的执行,所以InterceptorAttribute需要一个方法CreateInterceptor,其最终代码如下
public abstract class InterceptorAttribute : Attribute
{
#region 私有成员
private readonly int m_Order;
#endregion
#region 属性
public virtual int Order
{
get
{
return m_Order;
}
}
#endregion
#region 可重写方法
public abstract IInterceptor CreateInterceptor();
#endregion
#region 构造函数
public InterceptorAttribute()
{
}
public InterceptorAttribute(int order)
{
m_Order = order;
}
#endregion
}
4.特定的拦截器需要特定的Attribute
这个简单,对于我们的QueryStringCheckInterceptor,再做一个QueryStringCheckAttribute,继承自InterceptorAttribute,代码如下
其中的Message属性是用来抛异常的时候给异常的,大可不必理会
5.让页面认识Attribute
至此,我们的底层工作已经完成了,接下去需要的就是让页面找得到这些Attribute并可以执行逻辑
在此前有一篇文章中我提到过使用HttpHandlerFactory让WebForm集成Unity,这一次也是一样,使用HttpHandlerFactory将相应逻辑加到页面中
首先新建一个AspxHttpHandlerFacotry,实现IHttpHandlerFactory,其中的Dispose方法不需要执行任何逻辑,而GetHandler方法如下
{
Page page = (Page)BuildManager.CreateInstanceFromVirtualPath(url, typeof(Page));
Debug.Assert(
(page != null),
"AspxHttpHandlerFactory没有获取Page实例"
);
BuildPage(page);
return page;
}
这个实现并没有去反射获得PageHandlerFactory的实例,事实上这个实现方案是我反编译了System.Web.dll后看了PageHandlerFactory的实现大致搞出来的,不知道能不能不出错,总之在我试验阶段还是运行地好好的~
再看看其中的BuildPage方法,正是这个方法将相应的逻辑加到了Page中,因为Page在生命周期里给了很多的事件,而这里正要用到PreLoad和LoadComplete事件
{
Type typeOfPage = page.GetType();
IEnumerable<IInterceptor> interceptors = GetInterceptors(typeOfPage);
page.PreLoad += (sender, e) =>
{
interceptors.Each(interceptor => interceptor.ExecutePreLoad((Page)sender));
};
page.LoadComplete += (sender, e) =>
{
interceptors.Each(interceptor => interceptor.ExecutePostLoad((Page)sender));
};
}
这里涉及到2个方法,GetInterceptors方法获取页面上加的所有InterceptorAttribute,利用Order属性排序,再调用CreateInterceptor方法产生拦截器并返回,这个代码相当简单,不再一一展示了
Each方法,这个大可不必理会,是我无聊写的一个简单的方法,意思就是对IEnumerable里的每一个元素执行某一方法,这样不用每次都写foreach,代码看起来比较简洁
需要注意的是,在这个方法里,只能通过delegate或lambda去绑定事件,而不能直接将某个方法赋给事件,因为interceptors是需要在PreLoad和LoadComplete都用到的,这里必须将interceptors放在闭包里面
OK,到此为止,页面的拦截功能已经完成了
自动赋值功能的实现
1.定义赋值器的Attribute
赋值比较容易,这里直接定义一个InjectorAttribute,此Attribute同时负责将值注入到属性,当然为了方便,这个Attribute作为模板存在,子类只需要提供GetValue的实现,其他的过程都有基类完成,代码如下
2.实现自定义的赋值器
其实就是继承InjectorAttribute啦,很简单,也不多作说明了,以一个SessionInjectorAttribute为例
逻辑也相当简单,从Session取出相应的值就可以了
3.集成到WebForm
当然,可以用HttpHandlerFactory去绑定Load以前的事件来进行赋值
但是我们已经实现了对Load事件的拦截,为什么又要无聊地再去绑定别的事件来增加复杂度呢,因此这里最可行的方案是提供一个InterceptorAttribute的子类
{
#region 重写方法
public override IInterceptor CreateInterceptor()
{
return new InjectInterceptor();
}
#endregion
#region 构造函数
public InjectAttribute()
{
}
public InjectAttribute(int order)
: base(order)
{
}
#endregion
}
其中的InjectInterceptor实现如下
{
#region IInterceptor成员
void IInterceptor.ExecutePreLoad(Page page)
{
//获取所有的属性
//属性必须声明为public的,且有public的set方法,且属性必须是非静态的
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;
PropertyInfo[] properties = page.GetType().GetProperties(flags);
foreach (PropertyInfo property in properties)
{
if (property.CanWrite)
{
//获取属性上的Injector
IEnumerable<InjectorAttribute> injectors = GetInjectors(property);
//依次执行
foreach (InjectorAttribute injector in injectors)
{
//注入值
if (injector.Inject(property, page))
{
return;
}
}
}
}
}
/// <summary>
/// 已重写,不作任何处理..
/// </summary>
/// <param name="page">待处理的<see cref="System.Web.UI.Page"/>实例.</param>
void IInterceptor.ExecutePostLoad(Page page)
{
}
#endregion
//返回所有加于属性上的Injector
private IEnumerable<InjectorAttribute> GetInjectors(PropertyInfo property)
{
IEnumerable<InjectorAttribute> injectors =
property.GetCustomAttributes<InjectorAttribute>()
.OrderBy(attr => attr.Order)
.AsEnumerable();
return injectors;
}
}
逻辑也不难理解,遍历所有的属性,对需要赋值的都进行赋值
其中的GetCustomAttributes<TAttribute>泛型方法也不必理会,是一个扩展方法,就是为了少写几行码,我懒得很~
至此又完成了自动赋值,而不需要自定义一个BasePage之类的页面基类,实现了无侵入性的扩展,使用的时候只需要在页面上加[InjectAttribute]标签,在需要赋值的属性上加[SessionInterceptor]标签即可