使用Attribute简单地扩展WebForm

背景

WebForm的封装性很强,这一方面有利于面向构件的设计和应用,另一方面又使得扩展变得困难,此文将通过2个典型的例子来展示对WebForm的扩展,同时又不使用一个页面基类,仅仅通过外部方法对Page进行扩展。

第一点,对页面流程的限制

很多时候,我们要对页面的进入条件进行限制,比如以下地址

http://www.mywebsite.com/ViewPost.aspx?ID=3

这个地址需要在QueryString中带有键值为ID的值,如果没有此值,页面的执行将产生错误,这通常是一个NullReferenceException。

为了检测QueryString中是否存在ID,我们往往在Page_Load中写以下代码

if (String.IsNullOrEmpty(Request.QueryString["ID"]))
{
    
//处理错误
}

虽然代码本身并不负责,但是在十几个页面中连续地这么写是令人头疼的一件事,因此我们需要一种简单的方案,在我的方案中,我们在类上加上一个特定的Attribute即可,其代码如下

[QueryStringCheck(Key = "ID")]
public partial class Default : System.Web.UI.Page
{
    
//其他内容
}

显然这种声明式地编程相比之前的解决方案是令人愉快的,也使开发效率大大提高

 

第二点,对属性的自动注值

在以前例说明,当QueryString中确实存在ID的值的时候,我们就要取得此ID的值,因此我们往往会在Page_Load中写如下代码

string idStr = Request.QueryString["ID"];
//这里假设idStr是存在的
long id = default(long);
bool isParsed = Int32.TryParse(idStr, out id);
if (isParsed == false)
{
    
//处理错误
}
//使用id进行处理

显然这样的代码写十几次也是另人讨厌的,因此我们继续换一种方案,这次我们在属性上加特定的Attribute,结果如下

[QueryStringInjector("ID", ConverterType = typeof(Int64Converter))]
public int ID
{
    
private get;
    
set;
}

随后在Page_Load中可以直接使用ID,这同样令人愉快,也同样提高了开发速度


页面拦截功能实现方案

1.制作对页面的拦截器

首先定义一个IInterceptor接口,其方法可以对页面的Load事件进行拦截,在前后进行相应的处理

public interface IInterceptor
{
    
void ExecutePreLoad(Page page);

    
void ExecutePostLoad(Page page);
}

2.实现接口

有了接口,就到了自由发挥的时候了,以上面的检查QueryString为例,设计一个QueryStringCheckInterceptor

Code

方法的实现并不难,但是考虑到我们的Interceptor并不能真正地介入页面的流程控制其流程的停止与执行,为了向系统告知此页面因为不满足条件而产生错误,采用了异常的方式,在页面的QureyString不满足条件的情况下,抛出一个自定义的PageNotApplicableException即可

3.把拦截器加到页面上

这时自然要用到Attribute了,首先自定义一个Attribute,称之为InterceptorAttribute

当然还要示能从Attribute上取得拦截器以进行相关逻辑的执行,所以InterceptorAttribute需要一个方法CreateInterceptor,其最终代码如下

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
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,代码如下

Code

其中的Message属性是用来抛异常的时候给异常的,大可不必理会

5.让页面认识Attribute

至此,我们的底层工作已经完成了,接下去需要的就是让页面找得到这些Attribute并可以执行逻辑

在此前有一篇文章中我提到过使用HttpHandlerFactory让WebForm集成Unity,这一次也是一样,使用HttpHandlerFactory将相应逻辑加到页面中

首先新建一个AspxHttpHandlerFacotry,实现IHttpHandlerFactory,其中的Dispose方法不需要执行任何逻辑,而GetHandler方法如下

public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
    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事件


protected virtual void BuildPage(Page page)
{
    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的实现,其他的过程都有基类完成,代码如下

Code

2.实现自定义的赋值器

其实就是继承InjectorAttribute啦,很简单,也不多作说明了,以一个SessionInjectorAttribute为例

Code

逻辑也相当简单,从Session取出相应的值就可以了

3.集成到WebForm

当然,可以用HttpHandlerFactory去绑定Load以前的事件来进行赋值

但是我们已经实现了对Load事件的拦截,为什么又要无聊地再去绑定别的事件来增加复杂度呢,因此这里最可行的方案是提供一个InterceptorAttribute的子类

public sealed class InjectAttribute : InterceptorAttribute
{
    
#region 重写方法

    
public override IInterceptor CreateInterceptor()
    {
        
return new InjectInterceptor();
    }

    
#endregion

    
#region 构造函数

    
public InjectAttribute()
    {
    }

    
public InjectAttribute(int order)
        : 
base(order)
    {
    }

    
#endregion
}

其中的InjectInterceptor实现如下

public sealed class InjectInterceptor : IInterceptor
{
    
#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]标签即可

posted @ 2008-08-08 12:46  Gray Zhang  阅读(3533)  评论(24编辑  收藏  举报