INotifyCollectionChanged: ¡the new behaviour!

I have been working over the last three weeks on a project with WPF. You can see my last four post about the subject:

This time I will go with the INotifyCollectionChanged interface. 
Given the following domain:

public class Store : IStore
{ public virtual string Name { get; set; } [NotifyOnChange] public virtual IList<IProduct> Products { get; private set; } public Store() { Products = new List<IProduct>(); } public virtual void AddProduct(IProduct product) { product.Store = this; Products.Add(product); } public virtual void RemoveProduct(IProduct product) { product.Store = null; Products.Remove(product); } }

Note: NotifyOnChangeAttribute is mine. 
The mapping:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="uNHAddIns.Examples.CustomInterceptor"
                   namespace="uNHAddIns.Examples.CustomInterceptor.Domain">
  <class name="Store" >

    <id type="guid">
            <generator class="guid"/>
        </id>
    
        <property name="Name"/>
    
    <bag name="Products" cascade="all" inverse="true">
      <key column="StoreId" />
      <one-to-many class="IProduct" />
    </bag>
    
    </class>
</hibernate-mapping>
We need to meet the following test:

[Test]
public void NotifyOnChangeProperty_Should_Implement_INotifyCollectionChanged()
{
    var store = container.Resolve<IStore>();
    typeof(INotifyCollectionChanged).IsAssignableFrom(store.Products.GetType());
    typeof(INotifyPropertyChanged).IsAssignableFrom(store.Products.GetType());
}
[Test]
public void add_should_raise_collectionchanged()
{
    var store = container.Resolve<IStore>();
    IProduct newProduct = CreateNewProduct();
    bool eventWasRaised = false;

    ((INotifyCollectionChanged) store.Products)
        .CollectionChanged += (sender, args) =>
        {
            eventWasRaised = true;
            args.Action.Should().Be.EqualTo(NotifyCollectionChangedAction.Add);
            args.NewItems.Count.Should().Be.EqualTo(1);
            args.NewItems[0].Should().Be.EqualTo(newProduct);
        };


    store.AddProduct(newProduct);

    eventWasRaised.Should().Be.True();
}

I won't show you the full set of test for now.

Implementing the dynamicProxy interceptor:

public class CollectionPropertyInterceptor : IInterceptor
{

    private readonly IDictionary<string, object> _interceptedCollections
        = new Dictionary<string, object>();

    private object[] AttributeLookup(Type type, Type attribType, string propertyName) 
    {}

    #region IInterceptor Members

    public void Intercept(IInvocation invocation)
    {
        if (!invocation.MethodInvocationTarget.Name.StartsWith("get_"))
        {
            invocation.Proceed();
            return;
        }

        string propName = invocation.MethodInvocationTarget.Name.Substring(4);

        if (AttributeLookup(invocation.InvocationTarget.GetType(),
                            typeof (NotifyOnChangeAttribute),
                            propName).Length != 1)
        {
            invocation.Proceed();
            return;
        }


        object interceptedCollection;
        _interceptedCollections.TryGetValue(propName, out interceptedCollection);

        if (interceptedCollection != null)
        {
            invocation.ReturnValue = interceptedCollection;
        }
        else
        {
            invocation.Proceed();

            var itemType = invocation.ReturnValue.GetType()
                                     .GetGenericArguments()[0];

            var observableType = typeof (ObservableCollection<>)
                                        .MakeGenericType(itemType);

            object newInterceptedCollection = 
                Activator.CreateInstance(observableType, invocation.ReturnValue);

            invocation.ReturnValue = newInterceptedCollection;

            _interceptedCollections.Add(propName, newInterceptedCollection);
        }
    }

    #endregion
}
Ok, what I’m doing here? Let me explain, this interceptor intercept the “get_Products” method call, and enwrap the return type into an ObservableCollection. Then I store this collection in a dictionary for subsequent calls.

You need to register the Store entity as follows:

container.Register(Component.For<Store>()
                       .Interceptors(new InterceptorReference(
                           typeof (CollectionPropertyInterceptor))).Anywhere
                       .ImplementedBy<Store>()
                       .EnableNhibernateEntityCompatibility()
                       .LifeStyle.Transient);

container.Register(Component.For<IStore>()
                       .UsingFactoryMethod(kernel => kernel.Resolve<Store>())
                       .LifeStyle.Transient);
Using the EnhancedBytecode provider from unhaddins this test also works:

[Test]
public void add_should_raise_collectionchanged_on_nontrascient()
{
    var id = CreateFooStore();
    IProduct newProduct = CreateNewProduct();

    using (var session = sessions.OpenSession())
    using(session.BeginTransaction())
    {
        var store = session.Get<Store>(id);
        bool eventWasRaised = false;
        ((INotifyCollectionChanged)store.Products)
            .CollectionChanged += (sender, args) =>
                {
                    eventWasRaised = true;
                    
                    args.Action
                        .Should().Be.EqualTo(NotifyCollectionChangedAction.Add);
                    
                    args.NewItems.Count
                        .Should().Be.EqualTo(1);
                    
                    args.NewItems[0]
                        .Should().Be.EqualTo(newProduct);
                };


        store.AddProduct(newProduct);

        eventWasRaised.Should().Be.True();
    }

}

I'm going to merge these functions into one package with everything you need to work with wpf, nhibernate and the aforementioned interfaces. This package will be available at unhaddins.

blog comments powered by Disqus
  • Categories

  • Archives