上一篇
Unity&WebForm(1): 自定义IHttpHandlerFactory使用Unity对ASP.NET Webform页面进行依赖注入中让Unity和WebForm结合在一起,通过使用HttpHandlerFactory实现了对页面的依赖注入,本文将在上篇的基础上,通过对Unity的LifetimeManager的扩展实现从WEB Application特有的HttpContext中取值注入页面
背景
在很多情况下,会有较为持久地保存对象的需求,但由于对象的类度太细,也许不会考虑使用数据库,此时HttpContext的Items属性是一个很好的归宿,而在某些页面,则需要从HttpContext中获得此类对象,一个比较普遍的做法是使用属性进行封装,代码如下
public
UserInfo CurrentUser
{
get
{
return HttpContext.Current.Items["CurrentUser"] as UserInfo;
}
set
{
HttpContext.Current.Items["CurrentUser"] = value;
}
}
这样写的效果是显而易见的,避免了每一次接触CurrentUser都要写HttpContext.Current...之类的代码,但是就简洁性来说尚有不足,如果可以简单地写一个自动生成的属性,再由外界将HttpContext.Current.Items["CurrentUser"]注入其中,将会很大程度上提高程序编写的效率
原理
上一篇中已经将WebForm的页面和Unity合在了一起,在页面被创建之后即可由Unity对其进行注入,所以这次将在上一篇的基础上,通过扩展Unity实现功能。
在Unity中,如何获取对象的实例及如何销毁对象都是由LifetimeManager完成的,其定义如下
public
abstract
class
LifetimeManager : ILifetimePolicy, IBuilderPolicy
{
protected LifetimeManager();
public abstract object GetValue();
public abstract void RemoveValue();
public abstract void SetValue(object newValue);
}
其中GetValue方法获取对象实例,RemoveValue方法销毁对象,SetValue方法为对外引用的保存提供新的实例
有了这3个方法,就可以通过自定义LifetimeManager来实现从HttpContext中取值,但此处又会出现一个问题:
LifetimeManager如何得知需要取得的键值?
如果使用硬编码方式生成LifetimeManager的实例,那么在构造的时候传入键值即可,是十分方便的,但如果使用配置文件呢?
为了解决以上问题,就需要引入Unity中的另一个概念:TypeConverter
Unity为了解决在配置文件中写入值的问题,在定义Lifetime或者Property等参数的时候,会允许有一个typeConverter属性,Unity自动使用此属性中指出的继承自TypeConverter类的类实现字符串(保存于value属性)到具体对象的转换
因此,我们可以利用这一点,在value中保存需要的键值,再自定义TypeConverter生成LifetimeManager,也算是一个小小的Hack吧
实现
首先自定义一个LifetimeManager,在此叫ContextLifetimeManager,方法的实现非常简单,不多作解释,代码如下
public
class
ContextLifetimeManager : LifetimeManager
{
public string Key
{
get;
set;
}
public ContextLifetimeManager(string key)
{
Key = key;
}
public override object GetValue()
{
return HttpContext.Current.Items[Key];
}
public override void RemoveValue()
{
HttpContext.Current.Items[Key] = null;
}
public override void SetValue(object newValue)
{
HttpContext.Current.Items[Key] = newValue;
}
}
注意这只是最为简单的实现,没有考虑多线程同步等复杂的问题,也仅仅是作为一个演示,如果需要线程安全等特性,可以参考官方的ContainerControlledLifetimeManager(就是我们使用singleton作为生命周期时使用的),里面使用了Moniter控制线程同步
这里的问题便是如何将需要的Key传入到对象中,根据原理中说的,还需要一个TypeConverter,代码如下
public
class
ContextLifetimeManagerConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
string str = value as string;
if (!String.IsNullOrEmpty(str))
{
return new ContextLifetimeManager(str);
}
return base.ConvertFrom(context, culture, value);
}
}
开发过web控件的人对这种写法会非常熟悉,和web控件中的属性赋值一样,重写2个方法将字符串转换为所需要的类型,在此像是构造一个新的ContextLifetimeManager对象并提供字符串值作为键值
在配置文件中,可以将value属性设为所需的键值,再提供此TypeConverter即可,我的配置文件如下
Code
<unity>
<typeAliases>
<typeAlias alias="singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager, Microsoft.Practices.Unity"/>
<!-- 自定义的LifetimeManager的另名 -->
<typeAlias alias="context" type="GDev.Test.WebApp.Unity.ContextLifetimeManager, GDev.Test.WebApp"/>
</typeAliases>
<containers>
<container name="HttpHandlerContainer">
<types>
<type type="GDev.Test.WebApp.UserInfo, GDev.Test.WebApp" mapTo="GDev.Test.WebApp.UserInfo, GDev.Test.WebApp">
<!-- 使用自定义的ContextLifetimeManager,并利用typeConverter将key通过value传入 -->
<lifetime type="context"
value="CurrentUser"
typeConverter="GDev.Test.WebApp.Unity.ContextLifetimeManagerConverter, GDev.Test.WebApp" />
</type>
<!-- Default页面的依赖 -->
<type type="GDev.Test.WebApp.Default, GDev.Test.WebApp" mapTo="GDev.Test.WebApp.Default, GDev.Test.WebApp">
<lifetime type="singleton" />
<typeConfig extensionType="Microsoft.Practices.Unity.Configuration.TypeInjectionElement, Microsoft.Practices.Unity.Configuration">
<property name="CurrentUser" propertyType="GDev.Test.WebApp.UserInfo, GDev.Test.WebApp">
<dependency />
</property>
</typeConfig>
</type>
</types>
</container>
</containers>
</unity>
此处的键值为CurrentUser,向Default页面进行了注入
为了演示,修改Default页面的代码如下
public
partial
class
Default : System.Web.UI.Page
{
public UserInfo CurrentUser
{
get;
set;
}
protected void Page_Load(object sender, EventArgs e)
{
if (CurrentUser != null)
{
Response.Write("注入成功!");
}
}
}
在页面中有一个CurrentUser属性等待注入,其运行结果自然是显示了"注入成功",也就不再截图了
扩展
1.同样可以自定义SessionLifetimeManager,但需要注意的是不可以在HttpHandlerFactory中使用Session,因此要将BuildUp的时机放到页面的PreInit事件中,另外还需要注意的是,如果你将Unity和PIAB一起用,那么在页面的PreInit中使用container.BuildUp(GetType().BaseType, this);并无法提供PIAB的策略注入机制,其原因是PIAB的注入会返回一个全新的对象,而在些单纯地使用BuidUp仅仅是将this对象包装了起来
2.当然也可以有QueryStringLifetimeManager,不过这就更麻烦了,因为QueryString的值是string类型,要转换到其他类型还需要一个TypeConverter,一个可行的解决方案是在配置文件中的value项以一种约定的方式加入TypeConverter的类型,比如value="key&TypeConverterType"
总结
本文提供了对Unity的一种扩展的思路,本次扩展针对对象生命周期管理的环节,使其与WebForm更好地进行结合,事实上,如果愿意使用一个页面总基类并在页面的生命周期开始时再进行注入的话,还可以通过扩展BuilderExtension实现页面流程的控制,这将是下一次话题。