Can't undo lazy-loaded child loaded after BeginEdit (managed property)?

Can't undo lazy-loaded child loaded after BeginEdit (managed property)?

Old forum URL: forums.lhotka.net/forums/t/6420.aspx


rsbaker0 posted on Friday, February 13, 2009

I'm having what seems to be a related (but different) problem from the one I described a few weeks ago, and this is admittedly related to undo support for child objects which isn't currently fully supported. Still, it seems like this isn't quite right. I'm using CSLA 3.5.1, DisableIEditableObject=true.

This is a somewhat unusual lazy child scenario. The child isn't automatically loaded just because you call GetProperty(). The lazy loading occurs elsewhere, so a call to GetProperty() returns a null value if the object isn't loaded.

At the time I called BeginEdit(), it evidently pushed an IFieldData value onto the state stack for which the Value is null, so the object didn't exist before.

When the child is lazily loaded, it syncs up it's edit level correctly and every thing seems fine.

Now I call CancelEdit().

What I expected (and want) to happen is for this child to now disappear. It wasn't there when I called BeginEdit(), and now I want it to go away.

However, the undo code in FieldDataManager sees that there was previously an item on the state stack (not looking closer to see that it was really null, e.g. IFieldData.Value==null), so calls UndoChanges on the the lazy child instead of removing it.

I think in this case a reasonable course of action is to just restore the previous item from the state stack, e.g. just put back the IFieldData with the null value.

I think I can program around it for now, but it seems  to me that there is clearly enough information on the state stack for UndoChanges to detect that the previous object value was really null. However, I can see how this might be an edge case.

RockfordLhotka replied on Friday, February 13, 2009

I'm not sure it does have enough information to do what you want.

Remember this is n-level undo. I don't think it has enough information to know that the value was null at level n1, but you are now at n3 and so it will take three CancelEdit() calls to get back to the null.

rsbaker0 replied on Saturday, February 14, 2009

If the child object was null at n1 but present at n2 and n3, here is what I think happens: (and how a possible fix might work)

When you CancelEdit on the Parent at n3, the state stack has one of your "boolean placeholders" that just indicates the child object was present at n2, so you cascade the UndoChanges call to the current child value. Then you pop the state stack.

Then you CancelEdit on the Parent at n2. The prior state on the stack for the child now has IFieldData(null value) rather than the boolean. This is not tested for currently but I think it could be.  So, instead of cascading the call to the child like you do now, you could instead discard the child object entirely and restore the IFieldData reference as the child value in the FieldDataManager.

Now the Parent object is at n1, and it would (with the fix) look just like it did before the BeginEdit() BeginEdit() CancelEdit() CancelEdit() sequence.

RockfordLhotka replied on Saturday, February 14, 2009

You miss my point.

 

var root = Root.NewRoot();

root.BeginEdit();

root.Child = Child.NewChild();

root.BeginEdit();

root.BeginEdit();

root.BeginEdit();  // edit level of root and child are now at 4

 

root.CancelEdit();

 

This is where the problem occurs. At this point, how do we know whether to revert to the null value that the child started with, or to cascade the CancelEdit() call to the child?

 

I agree that this should be resolved. I’m just saying that it isn’t a trivial thing.

 

There’s already an item in the wish list that basically addresses this question – which is that a direct managed reference to a child object needs to act much more like a single item BusinessListBase reference to that child.

 

I mean really what you want is more than you are asking. Really what you want is to be able to do this:

 

var root = Root.GetRoot(123);

root.BeginEdit();

root.RemoveChild(); // removes existing child, which is marked for deletion and stored in a “deletedList”

root.Child = Child.NewChild();

 

// … (any number of nested Begin/Cancel/Apply edit operations

 

root.CancelEdit(); // removes new child, replacing it with the “deleted” child

 

This is the same problem as your null problem – just with an initial non-null value.

 

The problem is absolutely solvable. Either by having the FieldData object use a BusinessListBase internally to handle all the complexity, or by creating a ChildFieldData class that does much of what BLB does.

 

Rocky

 

rsbaker0 replied on Saturday, February 14, 2009

I think I did understand the point. I'm not asking for a perfect solution -- I think this is a 2-3 line fix. Also, this example works the way I want it to with a non-managed field, it just doesn't work with a managed field.

Using your example:

var root = Root.NewRoot();

root.BeginEdit();

root.Child = Child.NewChild();

root.BeginEdit();

root.BeginEdit();

root.BeginEdit();  // edit level of root and child are now at 4

root.CancelEdit();

 

At this point I don't see a problem -- the state stack in the FieldManager indicates a previous value was present, so you call UndoChanges() on the root.Child value. Then you pop the state stack.

 

The same would be true on the 2nd and 3rd calls to CancelEdit(). So, again you call UndoChanges() on the Child.

 

On a 4th call to CancelEdit(), matching the first BeginEdit(), however, the state stack tells you there wasn't a child value, so you would remove it.

 

 

RockfordLhotka replied on Saturday, February 14, 2009

Non-managed fields work correctly because the null value is stacked into the state – so CSLA actually knows at which point to switch back to the null.

 

I may address this in 3.6.2 – I just don’t know for sure – it is a matter of time and priority. But I need to fix it correctly, to address not only the null issue, but the delete issue too – because if I fix the delete issue the null issue is resolved by definition.

 

Rocky

 

 

From: rsbaker0 [mailto:cslanet@lhotka.net]
Sent: Saturday, February 14, 2009 3:51 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: Can't undo lazy-loaded child loaded after BeginEdit (managed property)?

 

I think I did understand the point. I'm not asking for a perfect solution -- I think this is a 2-3 line fix. Also, this example works the way I want it to with a non-managed field, it just doesn't work with a managed field.

Using your example:

var root = Root.NewRoot();

root.BeginEdit();

root.Child = Child.NewChild();

root.BeginEdit();

root.BeginEdit();

root.BeginEdit();  // edit level of root and child are now at 4

root.CancelEdit();

At this point I don't see a problem -- the state stack in the FieldManager indicates a previous value was present, so you call UndoChanges() on the root.Child value. Then you pop the state stack.

The same would be true on the 2nd and 3rd calls to CancelEdit(). So, again you call UndoChanges() on the Child.

On a 4th call to CancelEdit(), matching the first BeginEdit(), however, the state stack tells you there wasn't a child value, so you would remove it.

 

 



rsbaker0 replied on Saturday, February 14, 2009

RockfordLhotka:

Non-managed fields work correctly because the null value is stacked into the state – so CSLA actually knows at which point to switch back to the null.

I'm not trying to quibble, but (unless I'm missing something and I've waded in pretty deeply already) your managed field implementation has the same information. However, your test in the FieldDataManager to see if a previous value was in the state stack only looks to see if it there is *any* non-null value. This test gets confused if the previous "value" is an IFieldData w/null data value. It works fine if the field was actually never previously accessed and there is no IFieldData at all on the stack.

 

I went to managed fields for child objects per your recommendation because they are supposed to simplify the programming and you automatically get whatever fixes or improvements you put into the infrastructure. In this case, my old code worked as I expected it to (with the known limitations), and then suddenly broke just because I changed it to a managed property. 

 

Until you get around to a complete solution, I'm just asking that they work the same way.

 

 

RockfordLhotka replied on Saturday, February 14, 2009

Don’t worry about quibbling :)

 

I have not looked at that code for (probably) months and am going from memory.

 

If you have a 2-3 line fix, post the idea and that’ll help me when I get time to look at the issue.

 

Rocky

 

 

From: rsbaker0 [mailto:cslanet@lhotka.net]
Sent: Saturday, February 14, 2009 4:14 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: RE: Can't undo lazy-loaded child loaded after BeginEdit (managed property)?

 

Image removed by sender.RockfordLhotka:

Non-managed fields work correctly because the null value is stacked into the state – so CSLA actually knows at which point to switch back to the null.

I'm not trying to quibble, but (unless I'm missing something and I've waded in pretty deeply already) your managed field implementation has the same information. However, your test in the FieldDataManager to see if a previous value was in the state stack only looks to see if it there is *any* non-null value. This test gets confused if the previous "value" is an IFieldData w/null data value. It works fine if the field was actually never previously accessed.

I went to managed fields for child objects per your recommendation because they are supposed to simplify the programming and you automatically get whatever fixes or improvements you put into the infrastructure. In this case, my old code worked as I expected it to (with the known limitations), and then suddenly broke just because I changed it to a managed property.

 

 



rsbaker0 replied on Sunday, February 15, 2009

RockfordLhotka:

Don’t worry about quibbling :)

 

I have not looked at that code for (probably) months and am going from memory.

 

If you have a 2-3 line fix, post the idea and that’ll help me when I get time to look at the issue.

 

Rocky

 

Here is my current FieldDataManager.UndoChanges() implementation for 3.5.1. It has two fixes in it. The first fix is for this problem, the second fix  was where a previous non-null child is set to null and then UndoChanges() left a FieldData<bool>, causing an invalid cast. Both changes are commented.

 

    void Core.IUndoableObject.UndoChanges(int parentEditLevel, bool parentBindingEdit)
    {
      if (EditLevel > 0)
      {
        if (this.EditLevel - 1 < parentEditLevel)
          throw new UndoException(string.Format(Properties.Resources.EditLevelMismatchException, "UndoChanges"));

        IFieldData[] state = null;
        using (MemoryStream buffer = new MemoryStream(_stateStack.Pop()))
        {
          buffer.Position = 0;
          var formatter = SerializationFormatterFactory.GetFormatter();
          state = (IFieldData[])(formatter.Deserialize(buffer));
        }

        for (var index = 0; index < _fieldData.Length; index++)
        {
          var oldItem = state[index];
          var item = _fieldData[index];
          if (item != null)
          {
              var undoable = item.Value as IUndoableObject;
              if (undoable != null)
              {
                  // current value is undoable
                  if (oldItem != null)
                  {
                      // Change to CSLA here. If oldItem is an IFieldData with null value
                      // then there really wasn't an object here previously, so don't
                      // cascde the call. Just put the IFieldData reference back
                      if (oldItem.Value != null)
                          undoable.UndoChanges(parentEditLevel, parentBindingEdit);
                      else
                          _fieldData[index] = oldItem;
                  }
                  else
                      _fieldData[index] = null;
                  continue;
              }
              else
              {
                  // CHANGE TO CSLA HERE
                  // Workaround for previously existing child object being set to NULL
                  // NOTE: This does not make change to null undoable, but avoids leaving mismatching FieldData
                  // type in the slot formerly occupied by an object
                  if (item.Value == null && (oldItem != null && oldItem.GetType() != item.GetType()))
                      continue; // Leave existing FieldData with null value for now
              }
          }
          // restore IFieldData object into field collection
          _fieldData[index] = oldItem;
        }
      }
    }

Andreas replied on Wednesday, August 04, 2010

Hi,

has this issue been solved in one of the current releases? 
Is there any workaround available?
The following test still fails with Csla 8.3.4 Beta :

 

 

        public class BusinessRoot : BusinessBase<BusinessRoot>
        {
            protected BusinessRoot()   { }
            public static BusinessRoot Create()
            {
                return new BusinessRoot() { Name = "BusinessRoot" };
            }
            private static PropertyInfo<string> NameProperty = RegisterProperty<string>(o => o.Name);
            public string Name
            {
                get
                {
                    return this.GetProperty(NameProperty);
                }
               set
                {
                    this.SetProperty(NameProperty, value);
                }
            }
            private static PropertyInfo<BusinessChild> ChildProperty = RegisterProperty<BusinessChild>(o => o.Child);
            public BusinessChild Child
            {
                get
                {
                    return this.GetProperty(ChildProperty);
                }
                set
                {
                    this.SetProperty(ChildProperty, value);
                }
            }
        }
        public class BusinessChild : BusinessBase<BusinessChild>
        {
            protected BusinessChild()  {  }
            public static BusinessChild Create(string name)
            {
                return new BusinessChild() {Name = name};
            }

            private static PropertyInfo<string> NameProperty = RegisterProperty<string>(o => o.Name);
            public string Name
            {
                get
                {
                    return this.GetProperty(NameProperty);
                }
                set
                {
                    this.SetProperty(NameProperty, value);
                }
            }
        }

        [TestMethod]
        public void Undotest()
        {
            BusinessRoot b = BusinessRoot.Create();
            b.Child = BusinessChild.Create("Child1");
            Assert.IsTrue(b.Child.Name == "Child1", "Setting Child faild");
            b.BeginEdit();
            b.Child = BusinessChild.Create("Child2");
            Assert.IsTrue(b.Child.Name == "Child2", "Setting Child faild");
            b.CancelEdit();

            // FAILS !!!!
            Assert.IsTrue(b.Child.Name == "Child1", "Undo faild");
        }

 

 

RockfordLhotka replied on Wednesday, August 04, 2010

I think so, but I'm not sure. When you register a property in CSLA 4 (and maybe 3.8.4?) you can (and must) mark the property as lazy loaded - it is an extra property on the RegisterProperty() call. That tells CSLA to treat the property somewhat differently because it is lazy loaded.

None of your RegisterProperty() calls are indicating that the property is lazy loaded, so I wouldn't expect it to work.

Andreas replied on Thursday, August 05, 2010

The test fails in Csla 3.8.4 and Csla 4. And changing in the RelationshipType of RegisterProperty doesn't change the behavior. After looking in the source I suspect that child objects are simply ignored for serializing in FieldDataManager.CopyState(), because they are never inserted in the object state collection that is finally put on the stack - I am not sure if it is by purpose or if it is a bug.

RockfordLhotka replied on Thursday, August 05, 2010

Normally the child's state should not be part of the parent state. Each object in the object graph is responsible for managing its own state.

If you serialize/deserialize the child into the parent, then CancelEdit won't be an "in-place" operation anymore. Any UI elements (or anything else) referencing the child would need to rebind or recreate their reference to the child after CancelEdit. That's a major change compared to the behavior CSLA has had since its inception.

The "fake FieldData" object that's added to the parent is a placeholder for the child object so we know it exists and therefore should be processed in the undo step. If the child value was null (there was no child) then the placeholder isn't added, and the undo doesn't process it.

Andreas replied on Thursday, August 05, 2010

Does this basically mean, that changing the instances of child properties during an edit operation (like the unit test above) is not fully supported by the undo operation, because the FieldDataManager's UndoChanges() can't revert the child properties value (or instance) - with the only exception that it was null before BeginEdit() was called? If yes, will this be changed some day?

RockfordLhotka replied on Thursday, August 05, 2010

That is correct, that is not supported.

There is a broader semantic issue at work here. A child object is part of the object graph. So changing a child reference has broader meaning.

Ultimately your graph ends up tying back to some data store (99.999% of the time anyway). So changing an object reference must be reflected in the underlying data store when the object graph is saved. So just swapping out an object reference has to involve more than just changing the reference.

What happened to the previous instance? If it is no longer "part of the graph" then it semantically means it should be marked for deletion and so should actually be still part of the graph (just invisible). Like DeletedList in BLB.

My long-term intent (thought this has never become a real high priority) is for singular child references to automatically act very much like a BLB - which would address your requirement, while preserving the semantics you get when using a BLB - specifically meaning that when you change a child reference the previous instance is marked for deletion and put into a deleted list, so when the graph is saved that object's Child_DeleteSelf() is invoked.

So from a public perspective it would appear that the child instance has changed - but from an internal perspective there are now two (or more) instances being tracked. And when you do an undo operation it would basically back up a step - much like BLB does by undeleting and/or unadding items.

The work to do this is somewhat non-trivial obviously, and it isn't something tons of people are crying out to have, so it just hasn't floated to the top of my to-do list.

Copyright (c) Marimer LLC