José F. Romaniello

Las aventuras y desventuras de un codificador.

First of all you need to read my previous post and this from Fabio Maulo (a Italian guy who knows how to play truco better than me).
This time the behaviour is related to the IEditableObject. This interface has three methods BeginEdit, CancelEdit and EndEdit. Microsoft says:

Provides functionality to commit or rollback changes to an object that is used as a data source.

Note: it says “commit or rollback changes to an object”, not to a database.

Can we do it with a castle IInterceptor? YES.

The interceptor is defined as follows:

public class EditableObjectInterceptor : IInterceptor
   {
       private bool _isEditing;
       private readonly Dictionary<string, object> _propertyTempValues 
                                   = new Dictionary<string, object>();

       public void Intercept(IInvocation invocation)
       {
           switch (invocation.Method.Name)
           {
               case "BeginEdit":
                   _isEditing = true;
                   return;
               case "CancelEdit":
                   _isEditing = false;
                   return;
               case "EndEdit":
                   _isEditing = false;
                   AssignValues(invocation.InvocationTarget);
                   return;
           }
           if (!_isEditing)
           {
               invocation.Proceed();
               return;
           }
           if(!invocation.Method.Name.StartsWith("set_") 
               && !invocation.Method.Name.StartsWith("get_"))
               return;

           var isSet = invocation.Method.Name.StartsWith("set_");
           var propertyName = invocation.Method.Name.Substring(4);

           if(isSet)
           {
               _propertyTempValues[propertyName] = invocation.Arguments[0];
           }
           else
           {
               if (_propertyTempValues.ContainsKey(propertyName))
                   invocation.ReturnValue = _propertyTempValues[propertyName];
               else
                   invocation.Proceed();
           }
       }

       private void AssignValues(object target)
       {
           foreach (var propertyTempValue in _propertyTempValues)
           {
               var property = target.GetType().GetProperty(propertyTempValue.Key);
               property.SetValue(target, propertyTempValue.Value, null);
           }
       }
   }

If you use CommonDatastore (explained in my previous post) you add this behavior as follows:

container.Register(Component.For<IProduct>()
                            .AddEditableObjectBehavior()
                            .TargetIsCommonDatastore());

If you don’t use CommonDatastore:

container.Register(Component.For<Customer>()
                            .AddEditableObjectBehavior()
                            .EnableNhibernateEntityCompatibility());

Let the test speak for me:

        [Test]
        public void property_setters_should_work()
        {
            var product = container.Resolve<IProduct>();
            product.Description = "Potatoes";
            product.Description.Should().Be.EqualTo("Potatoes");

            product.BeginEdit();
            product.Description = "Banana";
            product.Description.Should().Be.EqualTo("Banana");
            product.Description = "Apple";
            product.Description.Should().Be.EqualTo("Apple");
            product.CancelEdit();
        }

        [Test]
        public void cancel_should_discard_changes_in_trascientobject()
        {
            var product = container.Resolve<IProduct>();
            product.Description = "Potatoes";
            product.Description.Should().Be.EqualTo("Potatoes");

            product.BeginEdit();
            product.Description = "Banana";
            product.Description.Should().Be.EqualTo("Banana");
            product.CancelEdit();

            product.Description.Should().Be.EqualTo("Potatoes");

        }

        [Test]
        public void endedit_should_push_changes_in_trascientobject()
        {
            var product = container.Resolve<IProduct>();
            product.Description = "Potatoes";
            product.Description.Should().Be.EqualTo("Potatoes");

            product.BeginEdit();
            product.Description = "Banana";
            product.EndEdit();

            product.Description.Should().Be.EqualTo("Banana");

        }

        [Test]
        public void cancel_should_discard_changes()
        {
            var id = CreatePotatoesProduct();
            using(var session = sessions.OpenSession())
            using(var tx = session.BeginTransaction())
            {
                var product = session.Get<IProduct>(id);

                product.Description.Should().Be.EqualTo("Potatoes");

                product.BeginEdit();
                product.Description = "Banana";
                product.Description.Should().Be.EqualTo("Banana");
                product.CancelEdit();

                product.Description.Should().Be.EqualTo("Potatoes");
            }
        }

        [Test]
        public void endedit_should_push_changes()
        {
            var id = CreatePotatoesProduct();
            using (var session = sessions.OpenSession())
            using (var tx = session.BeginTransaction())
            {
                var product = session.Get<IProduct>(id);

                product.Description.Should().Be.EqualTo("Potatoes");

                product.BeginEdit();
                product.Description = "Banana";
                product.Description.Should().Be.EqualTo("Banana");
                product.EndEdit();
                product.Description.Should().Be.EqualTo("Banana");
            }
        }
kick it on DotNetKicks.com

| More

13 comentarios:

Nice! This make edit/undo/cancel capability implicit for an object.

This could be used in the following scenario: an object is loaded from datastore, then a lot of system functions take place, such as "before showing a form for our loaded object, assign default fields, or invoke any custom logic", and then we could have a look at what exact properties where changed, and invoke business rules that trigget when some property changes. For example, when task status is changed to "finished", DateFinished should become now.

One question: how does it happen, that after getting the product from session, it is already implementing IEditableObject?
var product = session.Get(IProduct)(id);

Ok, i got it. NHibernate uses IInstantiator, so we provide custom instantiator, where we can inject as many interfaces to our IProduct as we want! :)

@Darius Damalakas: exactly. I'm working on uNhAddins.WPF and I have taken a different direction on that subject. Instead using IInstantiator + tuplizer, I'm using a nhibernate event + a EnhancedBytecode + ReflectionOptimizer. The cons of this new approach is that you can not map interfaces and abstract class.

well a total newbie here:

why do I ever need this thing?
I mean INotifyPropertyChanged on my entities?

I would welcome first the "problem" we are trying to solve and THEN one possible solution...

seems to complex to do when I don't have reason to. Otherwise excellent blog and Spanish is such a great language, Anónimo... wow how cool it sounds :)

this comment should gfo to previous post, sorry but it is all spanish and Firefox 3 is giving me very hard time to bypass captcha

Hi, José!
It seems to me that your interceptor stores all property values in a single dictionary, so, if you have multiple entities, their properties will be overwritten, is that so?
Thanks,
Ricardo Peres

José F. Romaniello dijo...

No, it isn't. Because I've an instance of the interceptor per-entity.
Also remember this API is deprecated. I've a done a lot of work on unhaddins, and now you can configure once an entity, and have this behavior even if you do Session.Load or Session.Get... Or whatever. Even, without put your entities in the container.
I hope can write about this topic on the mean time.

I don't understand... how do you have an interceptor per each entity? Don't you use a NHibernate interceptor when you create a session, that installs your interceptor?
Thanks for your answer, by the way!

José F. Romaniello dijo...

Ricardo, IInterceptor the one that I inherit from the
EditableObjectInterceptor is not from NHibernate, is from Castle..This is a
dynamicproxy IInterceptor. http://digg.com/u1Dsyo

Yes, José, I know!  ;)
What I don't know is how you inject this interceptor.

José F. Romaniello dijo...

Have a look to the line 20:
http://digg.com/u1DuYr
I setup my entities in the guywire.

José F. Romaniello dijo...

BTW, the current api is a little bit different. 
line 28 to 30:
http://digg.com/u1DuZE

I will write a post this week.

Sorry, I find out what I was doing wrong... I had an EmptyInterceptor which was also Castle's IInterceptor implementation, so on CreateClassProxy I was passing this as the interceptor argument! :)
Thanks for your help!

Publicar un comentario