Advance: Extending NHibernate Proxies
There was some interest in extending the way NHibernate deals with the entities, and I have just commited those changes into the trunk. Notice that since NHibernate 1.2.0 is at feature freeze right now, this will not be in 1.2.0 RTM, but at a later version.
At any rate, let us see how we can extend NHibernate so it would offer automatic implementation for INotifyPropertyChanged. Some caveats: It works only for entities that have lazy loading enabled (the default for 1.2.0) which were retreived from NHibernate.
You can see the full test here, but let me go through the implementation. First here is the test case:
[Test]
public void CanImplementNotifyPropertyChanged()
{
using (ISession s = OpenSession())
{
Blog blog = new Blog("blah");
Assert.IsFalse(blog is INotifyPropertyChanged);
s.Save(blog);
s.Flush();
}
using (ISession s = OpenSession())
{
Blog blog = (Blog)s.Load(typeof(Blog), 1);
INotifyPropertyChanged propertyChanged = (INotifyPropertyChanged)blog;
string propChanged = null;
propertyChanged.PropertyChanged += delegate(object sender, PropertyChangedEventArgs e)
{
propChanged = e.PropertyName;
};
blog.BlogName = "foo";
Assert.AreEqual("BlogName", propChanged);
}
}
As you can see, when we create the entity manually, it does not implements INotifyPropertyChanged. But when we load the object from NHibernate, not only does it implements INotifyProperyChanged, but it behaves correctly as well. They key is in extending the Proxy Factory that NHibernate uses for lazy loading.
We need to specify the proxy factory type that we would like to use:
protected override void BuildSessionFactory()
{
cfg.SetProxyFactoryClass(typeof(DataBindingProxyFactory));
base.BuildSessionFactory();
}
And now, let us see how the DataBindingProxyFactory works:
public class DataBindingProxyFactory : CastleProxyFactory
{
public override INHibernateProxy GetProxy(object id, ISessionImplementor session)
{
try
{
CastleLazyInitializer initializer = new DataBindingInterceptor(_persistentClass, id,
_getIdentifierMethod, _setIdentifierMethod, session);
object generatedProxy = null;
ArrayList list = new ArrayList(_interfaces);
list.Add(typeof(INotifyPropertyChanged));
System.Type[] interfaces = (System.Type[])list.ToArray(typeof(System.Type));
if (IsClassProxy)
{
generatedProxy = _proxyGenerator.CreateClassProxy(_persistentClass, interfaces, initializer, false);
}
else
{
generatedProxy = _proxyGenerator.CreateProxy(interfaces, initializer, new object());
}
initializer._constructed = true;
return (INHibernateProxy)generatedProxy;
}
catch (Exception e)
{
log.Error("Creating a proxy instance failed", e);
throw new HibernateException("Creating a proxy instance failed", e);
}
}
}
Here we did two things, We specify a different interceptor, which inherits from the default interceptor, that handles lazy loading, and we specify that the proxy that NHibernate returns should also implement INotifyPropertyChanged. Now, we need to see how the DataBindingInteceptor works:
public class DataBindingInterceptor : CastleLazyInitializer
{
private PropertyChangedEventHandler subscribers = delegate { };
public DataBindingInterceptor(System.Type persistentClass, object id,
MethodInfo getIdentifierMethod, MethodInfo setIdentifierMethod, ISessionImplementor session)
: base(persistentClass, id, getIdentifierMethod, setIdentifierMethod, session)
{
}
public override object Intercept(IInvocation invocation, params object[] args)
{
if (invocation.Method.DeclaringType == typeof(INotifyPropertyChanged))
{
PropertyChangedEventHandler propertyChangedEventHandler = (PropertyChangedEventHandler)args[0];
if (invocation.Method.Name.StartsWith("add_"))
{
subscribers += propertyChangedEventHandler;
}
else
{
subscribers -= propertyChangedEventHandler;
}
return null;
}
object result = base.Intercept(invocation, args);
if (invocation.Method.Name.StartsWith("set_"))
{
subscribers(this, new PropertyChangedEventArgs(invocation.Method.Name.Substring(4)));
}
return result;
}
}
We extend CastleLazyInitializer, which handles the usual lazy loading related logic for NHibernate, and when we get a method, we check to see if it a call to a method from the INotifyPRopertyChanged interface. If it is, we handle the add/remove of the event subscriber. But that is not the interesting part.
The interesting part is that after we let the method be processed normally (which is what the call to base.Intercept() does, we check to see if this is a property setter, and raise the appropriate event if it does.
The client code for this is very natural, and it took about 70 lines of code to add this functionality.
Comments
Would it be hard to create some "generic" InterceptableProxyFactory, that is configurable from outside with a list of additional interfaces and a list of interceptors that implement methods like PreIntercept (basicly the code before "object result = base.Intercept(invocation, args);" and PostIntercept (after that)?
And another question (sorry -- I am not fluent with NHibernate internals) -- would your example work with "nosetter" or "field" strategies as well?
Thanks,
andyqp
Yes, it would be easy to create InterceptableProxyFactory.
And this example would work for other access strategies, because I am not trying to raise an event when NH is changing the value, I am raising the event whenever the property is changed by YOUR code.
Does this work with ActiveRecord too?
if yes, could you give me 2 rows of example on how can I use it?
Thanks
Alessandro
i don't see that much of an advantage if not all instances of a certain class behave the same, i.e. implement INotifyPropertyChanged. if i need to distinguish between new an persistent objects what is the gain?
@Alessandro
Yes, it would.
The difference is that you would need to handle the ActiveRecordStarter event for models created and modify the Configuration object to register the new proxy factory, and then NH will handle everything else.
Comment preview