NHibernate & INotifyPropertyChanged
One of the things that make NHibernate easy to use is that it fully support the POCO model. But one of the things that most people do not consider is that since NHibernate did the hard work of opening up the seams to allow external persistence concerns, we can use the same seams to handle similar infrastructure chores externally, without affecting the POCO-ness of our entities.
Allow me to show you what I mean. First, we create the following factory, which makes use of Castle Dynamic Proxy to weave in support for INotifyPropertyChanged in a seamless manner:
public static class DataBindingFactory { private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator(); public static T Create<T>() { return (T) Create(typeof (T)); } public static object Create(Type type) { return ProxyGenerator.CreateClassProxy(type, new[] { typeof (INotifyPropertyChanged), typeof (IMarkerInterface) }, new NotifyPropertyChangedInterceptor(type.FullName)); } public interface IMarkerInterface { string TypeName { get; } } public class NotifyPropertyChangedInterceptor : IInterceptor { private readonly string typeName; private PropertyChangedEventHandler subscribers = delegate { }; public NotifyPropertyChangedInterceptor(string typeName) { this.typeName = typeName; } public void Intercept(IInvocation invocation) { if(invocation.Method.DeclaringType == typeof(IMarkerInterface)) { invocation.ReturnValue = typeName; return; } if (invocation.Method.DeclaringType == typeof(INotifyPropertyChanged)) { var propertyChangedEventHandler = (PropertyChangedEventHandler)invocation.Arguments[0]; if (invocation.Method.Name.StartsWith("add_")) { subscribers += propertyChangedEventHandler; } else { subscribers -= propertyChangedEventHandler; } return; } invocation.Proceed(); if (invocation.Method.Name.StartsWith("set_")) { var propertyName = invocation.Method.Name.Substring(4); subscribers(invocation.InvocationTarget, new PropertyChangedEventArgs(propertyName)); } } } }
Now that we have this, we can start creating entities that support INotifyPropertyChanged simply by calling:
var customer = DataBindingFactory.Create<Customer>();
This customer instance supports change notifications, but it is not something that we had to do, and it is something that we can pick & choose. If we want to use the same entities in a different context, where we don’t need INPC, we can simply skip using the factory, and not deal with it at all.
Now, using the data binding factory is good when we create the instances, but how are we going to teach NHibernate that it should use the factory when creating entities? That is actually quite easy, all we need to do is write an interceptor:
public class DataBindingIntercepter : EmptyInterceptor { public ISessionFactory SessionFactory { set; get; } public override object Instantiate(string clazz, EntityMode entityMode, object id) { if(entityMode == EntityMode.Poco) { Type type = Type.GetType(clazz); if (type != null) { var instance= DataBindingFactory.Create(type); SessionFactory.GetClassMetadata(clazz).SetIdentifier(instance,id, entityMode); return instance; } } return base.Instantiate(clazz, entityMode, id); } public override string GetEntityName(object entity) { var markerInterface = entity as DataBindingFactory.IMarkerInterface; if (markerInterface != null) return markerInterface.TypeName; return base.GetEntityName(entity); } }
Note that this interceptor does two things, first, it handles instantiation of entities, second, it make sure to translate data binding entities to their real types. All we are left now is to set the interceptor and we are done. Again, this is an opt in option, if we don’t want it, we can just not register the proxy, and we don’t worry about it.
Something that will probably come up is why not use NHibernate’s own proxy generation (byte code provider) to provide this facility. And the answer is that I don’t think it would be as easy as that. The byte code provider is there to provider persistence concerns, and trying to create a byte code provider that does both that and handle INPC issues is possible, but it would be more complicated.
This is a simple and quite elegant solution.
Comments
This is like 5th impl of INPC via DynamicProxy I've seen in last month :) Seems everyone is doing it. Anyway, this is a great example, although I would split all the logic you put into the interceptor, between InterceptorSelector, the interceptor and use mixin for the actual INPC implementation. I understand however that you probably did it in one place to keep the example short.
I really like this implementation. As you said, it is simple and elegant.
Is there a way for NHibernate to instantiate BindingList<T> for POCO properties of type IList<T> ?
I've used a similar approach on a project last year. It was more of a mapping between a ViewModel described just as an interface and some POCO. The interface would implement INPC and its nature allowed us to do funny things like declaratively adding Undo, Commit & Rollback functionality towards an associated POCO.
Here is a post on the same issue, he also has INotifyCollectionChanged etc. very cool posts.
jfromaniello.blogspot.com/.../...anged-as-aop.html
Dmitry,
Yes, sort of. You would need to write a accessor, take a look at how NHibernate Generics was implemented.
Besides of what Tuna said, I implement the code in unhaddins.wpf to work with INotifyPropertyChanged, INotifyCollectionChanged, IEditableObject (two implementatios). For InotifyPropertyChanged my IInterceptor is very similar (although I have another interceptor for the entity name thing)... and I use another extension point of nhibernate to inject the proxy...
One question. How does this way of creating entities go together with aggregates (as in DDD)? You can't have the agg POCO create a proxied POCO this way.
Jernej,
I have no idea, ISTR that DDD mentions factories, but regardless, I don't care about DDD in this context.
Nice post Oren! I like the way you went further and added nested subscriptions. One thing though, this requires all the props to be virtual.
Another idea I had been toying with to some success was to use mixins to create a proxy that delegates to a poco rather than deriving from it.. This way the poco does not need to have virtual props. You could take the approach further to then allow specifying exactly which members are exposed in the ViewModel rather than automatically adding all of the members to the VM surface.
Any thoughts on this?
Glenn
Glenn,
Virtuals are not an issue, NH already has that requirement.
Type wrapping is actually something we are considering for DP 3.0
The problem is with instance management and leaking this at that point.
I'm really excited about getting this to work, but i've run into a problem implementing. My domain objects are in a different project from my repositories, which I believe causes all the Type.GetType(clazz) to return null.
I was able to get around it by using
but
SessionFactory.GetClassMetadata(clazz).SetIdentifier(instance, id, entityMode);
errors with a null reference exception. I tried to switch from clazz, to type that was returned from FindType, but same result.
Any ideas?
Jon,
Not off the top of my head.
Try creating a small test case that shows this.
Turns out the issue was that SessionFactory on the DataBindingInterceptor was null. I had been setting the interceptor in the fluent buildup of the factory
.ExposeConfiguration(config=>config.SetInterceptor(new DataBindingInterceptor()))
Instead I modified my OpenSession method to:
IInterceptor dataBinding = new DataBindingInterceptor {SessionFactory = factory};
Works great now.
Jon,
All you need to do is add one line of code to your Fluent Buildup. Here's what mine looks like:
var intercepter = new DataBindingIntercepter();
<datasource())
That last line sets the SessionFactory on the interceptor.
Oren,
I ran into a problem with this that is really strange (but maybe makes sense).
When I call Get <t to fetch an object from NHibernate, this works and I can cast it to INotifyPropertyChanged and perform the wire-up. When I use Load <t, this does not work. The DataBindingIntercepter's Instantiate() method is not getting called. Now, Load() works quite a bit differently than Get(), as you've pointed out on your blog recently. But I am wondering if there is a way to make it work for Load() as well?
Chris,
This is because we aren't creating an instance yet.
If you want Load to support it as well you need to provide a ProxyFactoryFactory implementation that will support it.
In other words, there are two places that you need it. In the interceptor (when NH create the actual instance) and in the proxy factory (when NH creates the proxy)
Okay, that makes sense. I figured it had to be something like that; I am just not very familiar yet with how NHibernate does things yet.
Thanks Oren!
Comment preview