使用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

ContractedBlock.gif ExpandedBlockStart.gif Code
public sealed class QueryStringCheckInterceptor : IInterceptor
{
    
#region 私有成员

    
private string m_Key;

    
private string m_Message;

    
#endregion

    
#region 属性

    
public string Key
    {
        
get
        {
            
return m_Key;
        }
    }

    
public string Message
    {
        
get
        {
            
return m_Message;
        }
    }

    
#endregion

    
#region IInterceptor成员

    
public void ExecutePreLoad(Page page)
    {
        
if (String.IsNullOrEmpty(page.Request.QueryString[Key]))
        {
            
throw new PageNotApplicableException(
                page.Title, page.Request.Path, String.Format(Message, page.Title, Key));
        }
    }

    
public void ExecutePostLoad(Page page)
    {
    }

    
#endregion

    
#region 构造函数

    
public QueryStringCheckInterceptor(string key, string message)
    {
        m_Key 
= key;
        
if (String.IsNullOrEmpty(message))
        {
            m_Message 
= "必须在url中指定\"{1}\"才可进入\"{0}\"";
        }
        
else
        {
            m_Message 
= message;
        }
    }

    
#endregion
}

方法的实现并不难,但是考虑到我们的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,代码如下

ContractedBlock.gif ExpandedBlockStart.gif Code
public sealed class QueryStringCheckAttribute : InterceptorAttribute
{
    
#region 属性

    
public string Key
    {
        
get;
        
set;
    }

    
public string Message
    {
        
get;
        
set;
    }

    
#endregion

    
#region 重写方法

    
public override IInterceptor CreateInterceptor()
    {
        
return new QueryStringCheckInterceptor(Key, Message);
    }

    
#endregion

    
#region 构造函数

    
public QueryStringCheckAttribute()
    {
    }

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

    
#endregion
}

其中的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的实现,其他的过程都有基类完成,代码如下

ContractedBlock.gif ExpandedBlockStart.gif Code
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public abstract class InjectorAttribute : Attribute
{
    
#region 抽象方法

    
public abstract object GetValue(Type targetType);

    
#endregion

    
#region 方法

    
public bool Inject(PropertyInfo property, Page page)
    {
        
object value = GetValue(property.PropertyType);
        
return SetValue(property, page, value);
    }

    
protected virtual bool SetValue(PropertyInfo property, Page page, object value)
    {
        
if (value == null)
        {
            
return false;
        }
        
try
        {
            property.SetValue(page, value, 
null);
            
return true;
        }
        
catch (Exception)
        {
            
return false;
        }
    }

    
#endregion

    
#region 属性

    
public virtual int Order
    {
        
get;
        
set;
    }

    
#endregion
}

2.实现自定义的赋值器

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

ContractedBlock.gif ExpandedBlockStart.gif Code
public class SessionInjectorAttribute : InjectorAttribute
{
    
#region 私有成员

    
private readonly string m_Key;

    
#endregion

    
#region 属性

    
public string Key
    {
        
get
        {
            
return m_Key;
        }
    }

    
#endregion

    
#region 重写方法

    
public override object GetValue(System.Type targetType)
    {
        
return HttpContext.Current.Session[Key];
    }

    
#endregion

    
#region 构造函数

    
public SessionInjectorAttribute(string key)
    {
        m_Key 
= key;
    }

    
#endregion
}

逻辑也相当简单,从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]标签即可

转载于:https://www.cnblogs.com/GrayZhang/archive/2008/08/08/1263656.html

猜你喜欢

转载自blog.csdn.net/weixin_33939843/article/details/93272179