N-Level Undo Trouble - again! - Rocky can you explain?

N-Level Undo Trouble - again! - Rocky can you explain?

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


razorkai posted on Saturday, November 11, 2006

Hi folks.

I expect this issue is down to me doing something wrong, but I'm having a nightmare trying to sort it out.  I have a collection of editable business objects bound to a grid.  The grid is readonly, and is only used for display/selection of the record to be edited.  The form has an edit button that makes lots of other buttons available and also does a BeginEdit on the collection object.  The reason for this is so that I can let the user make lots of changes and then choose to save or discard them all.  Personally I don't like this approach, but this is what I have been asked for.  Anyway the BeginEdit call puts all objects in the collection at EditLevel 1.  Another button will open a modal form that takes the object that is selected in the grid in its constructor.  The modal form presents several fields from the object and allows editing them.  The fields are presented via text boxes and bound via a BindingSource component.  The act of setting the BindingSource's DataSource property to my object triggers a second BeginEdit call, taking my object to EditLevel 2.  This is fine, all ok so far.

The trouble comes when the user closes the form.  At this point I want to either accpet or cancel the user's chanegs based on whether they cancelled the form or not.  Rocky recommends calling CancelEdit or EndEdit on the BindingSource, so this is what I do.  Stepping through the code, CancelEdit leads into UndoableBase.UndoChanges. 

I watch as this lowers the edit level to 1 and goes on through to the BusinessBase.UndoChangesComplete.  At this point the edit level is still 1, which is correct.  UndoChangesComplete calls OnUnknownPropertyChanged which in turn calls OnPropertyChanged passing an empty string. 
When we hit the following line of code within this method, the edit level of my object jumps back up to 2!

_nonSerializableHandlers.Invoke(this, new PropertyChangedEventArgs(propertyName));

WHY?

This problems is screwing me up as no matter what I do I can't seem to make my UI work the way I need to.  AM I doing something stupid?  Please help if you can.

John.

RockfordLhotka replied on Saturday, November 11, 2006

My guess is that the "read-only" grid on the main form is raising the edit level for the currently selected row. I don't know that for sure, but that's my guess.

razorkai replied on Sunday, November 12, 2006

I have to admit I'm always surprised by the lack of responses when I post questions about N-Level Undo here.  Normally there are at least a few responses, but this subject never seems to raise much interest.  Is this because not many people are using the feature?

RockfordLhotka replied on Sunday, November 12, 2006

I think it is because the complexity isn't actually in n-level undo, as much as in Windows Forms 2.0 data binding, and the way it uses IEditableObject. All the issues raised by you and others thus far have ultimately been resolved by digging into the way data binding is invoking the n-level undo capability - and researching that is somewhat of a PITA... Sad [:(]

razorkai replied on Sunday, November 12, 2006

From my testing it seems that data binding invokes IEditableObject at unpredictable moments, particularly when using third party components.  This is fine if you are doing a simple undo function as the Csla is coded to try and deal with the issue, but it can mess you up totally if you are relying on on object being at a certain edit level at a given moment.  For example, I am using a Developer Express XtraGrid on a tab that starts off hidden.  When I assign an object to the grid's bindingsource DataSource property BeginEdit gets called via IEditableObject- so far this is normal.  The thing is that when you switch to the tab which the grid is on ApplyEdit gets called via IEditableObject.  This means that if I happen to be on the grid's tab when I assign an object to the BindingSource my object ends up at a different EditLevel to when I do it when the tab is not visible.

It is this sort of unpredictability that is causing me so many problems.  I just tried commenting out the IEditableObject stuff from the Csla and was able to get my N-Level Undo to function exactly how I want it to.  The drawbacks of course being a) I have modified the Csla, b) Objects will not function correctly in an object being edited in place within a grid.  I have just read a post in the old forum about someone who implemented a property, EnableIEditableObject, to let each object switch the behaviour on or off.  Maybe I'll look further down this route, as this could be an ok solution.  I don't suppose there is any chance of a similar switch being added to the framework is there?

RockfordLhotka replied on Monday, November 13, 2006

razorkai:

I don't suppose there is any chance of a similar switch being added to the framework is there?



Maybe. It seems to be a recurring issue - though I fear that the solution may be worse than the problem. I can just see people turning off the normal behavior and posting here wondering why data binding isn't working right...

I think I'd do it with an enum

_customer.UndoMode = UndoModes.Manual
_customer.UndoMode = UndoModes.DataBinding

or something like that. If there's a way to make the switch's meaning clear, I'd prefer that.

DansDreams replied on Wednesday, December 27, 2006

RockfordLhotka:
I think it is because the complexity isn't actually in n-level undo, as much as in Windows Forms 2.0 data binding, and the way it uses IEditableObject. All the issues raised by you and others thus far have ultimately been resolved by digging into the way data binding is invoking the n-level undo capability - and researching that is somewhat of a PITA... Sad [:(]

I've avoided editable grids like the plague before now, but I have some very simple editing of contacts' phone numbers that seems appropriate to allow in the grid.

I have spent way too many hours trying to track down this BeginEdit/ApplyEdit/CancelEdit behavior of the DevExpress grid using CSLA.  I have a Account:BusinessBase BO with a child OwnersList:BusinessListBase containing Owner:BusinessBase objects.  A common scenario I believe.

The grid is very generous with spewing out these method calls and twisting the EditLevels all up.  In standard grid fashion, navigating from one grid row to another is considered as an indication to save any changes on that row and ApplyEdit gets called. 

In another thread I read Rocky advising to let Windows data binding do it's thing as much as possible rather than try to manually control it, but that clearly doesn't work.

Is there a standard 1-2-3 step procedure that someone has found to consistently make grids work in this situation?  There seems to be several threads on the subject but no definitive guidance.

pelinville replied on Thursday, December 28, 2006

Something about all devexpress controls I have found is that they do it "their" way.  This is both good and bad.
 
For example the grids do not, by default, use the IBindingList sorting methods you may create.  I have found also that other expected behaviors just do not act the way you would expect.
 
If you purchased the source code for the controls I suggest you compile it and step through it to find out exactly what they are doing.  I have found that often it isn't crazy but it is not "standard" behavior. And see if there are updates to the controls you are using.  The early 2005 versions simply sucked. Sometimes I have been able to either override the behavior (DevExpress at least builds good overridability in) or find another way.  Their support is horrible.

DansDreams replied on Friday, December 29, 2006

Are you saying that with the standard Microsoft DataGridView you can just plunk it on a form and set a BindingSource up to point to the child collection as in my scenario and it works exactly like you'd expect with the final save and cancel authority still the parent root object?  I would have expected from the other comments that this isn't the case, or is everybody that's having trouble using 3rd party controls?

ajj3085 replied on Friday, December 29, 2006

Well the easiest way to determine this is to try it out and see.  It would also help you figure out if its the grid that's behaving badly.

FWIW, it seems most of the problems are with the devex grid.  I don't recall others having problems with the infragistics or the standard DGV once their code was setup properly.

RockfordLhotka replied on Friday, December 29, 2006

I have done exactly this many times - with the standard DataGridView - and it works as expected. I haven't directly tested this with any 3rd party grids, so I suspect that the issues are due to some inconsistency with the DevExpress grid, where they don't follow the pattern used by Microsoft.
 
If you can identify the pattern they do use, and if it doesn't conflict with Microsoft's pattern from the perspective of BLB, I'm happy to try and alter BLB to support their pattern too.
 
Rocky
 


From: DansDreams [mailto:cslanet@lhotka.net]
Sent: Friday, December 29, 2006 7:06 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] N-Level Undo Trouble - again! - Rocky can you explain?

Are you saying that with the standard Microsoft DataGridView you can just plunk it on a form and set a BindingSource up to point to the child collection as in my scenario and it works exactly like you'd expect with the final save and cancel authority still the parent root object?  I would have expected from the other comments that this isn't the case, or is everybody that's having trouble using 3rd party controls?


DansDreams replied on Friday, December 29, 2006

I just made up a simple form using only MS controls including the DataGridView and it exhibits the same behavior.

I modify the first row in the grid, then navigate to the second row, then back to the first.  Hit the main cancel button which calls EndEdit on the main/root BindingSource and the first row of the grid does NOT revert back to its original state.  In fact, it gets downright bizarre from the user's perspective... The original value in the first row is "hello".  I change it to "hi".  I click on the second row.  I then click back on the first row and change the value to "bye".  I hit the form's main Cancel button and it goes back to "hi" not to "hello".

I can see that the MS grid is also very generous with its BeginEdit and ApplyEdit calls every time the row selection is changed, so I don't think this is a DevExpress problem.  Could still be something boneheaded I've done of course.  I'm still trying to debug from that perspective.

Can anyone verify that this specific chain of events works ok in their application?

On a more theoretical note, one thing that this has shown me is that apparently there is going to be some extra memory gobbled up at the very least, my problem notwithstanding, because the CopyState cascades from the IEditableObject.BeginEdit on the root object and also gets called through the explicit IEditableObject.BeginEdit call on each row.

As someone who will likely never use the n-level undo complexity, I'd like to offer my thoughts on the "switch" discussed previously in this thread.  I would prefer a config switch that would limit the state stacking to a single level, effectively turning n-level undo to just simple "original state" tracking.  A property accepting an enum parameter like you described might be useful, but I don't want to think about it all the times it wouldn't.  In any event, I want to leave the IEditableObject interface working as written, because I think (at this point) that if the undo wasn't doing all the EditLevel tracking it would solve that problem anyway.  Maybe the uniqueness of each person's situation would require both, but I offer that there were several threads on the old forum wherein people suggest n-level was overkill.

ajj3085 replied on Friday, December 29, 2006

DansDreams:
Hit the main cancel button which calls EndEdit on the main/root BindingSource and the first row of the grid does NOT revert back to its original state.


What "main cancel button?"  I don't recall the DGV having a cancel button.  What happens if you just hit ESC?  Or is that what you mean by "main cancel button?"

If you put in a button to call CancelEdit on the collections object, I'd remove it.  The grid should work as expected without you having to put code in to do any of that.

Also, what does GetIdValue return?  If you're using ints, you'll need to keep them unique.

DansDreams replied on Friday, December 29, 2006

ajj3085:
DansDreams:
Hit the main cancel button which calls EndEdit on the main/root BindingSource and the first row of the grid does NOT revert back to its original state.


What "main cancel button?"  I don't recall the DGV having a cancel button.  What happens if you just hit ESC?  Or is that what you mean by "main cancel button?"

If you put in a button to call CancelEdit on the collections object, I'd remove it.  The grid should work as expected without you having to put code in to do any of that.

Also, what does GetIdValue return?  If you're using ints, you'll need to keep them unique.

Remember, my scenario is a BusinessBase object containing a BusinessListBase collection of BusinessBase objects:  Account -> OwnerList -> Owner.  My form is for editing the Account and includes a grid for editing the Owners among the controls for editing the Account itself.  So my "main" Cancel button is the one for the whole form - at the Account level.

I'm only editing objects retrieved from the database, so there's no issue with IDs.

I understand part of the problem here may be the way people design their UI.  By that I mean some people may have never thought to have a form like I do.  But because I generally think Microsoft's grids have expected way too much from the user what I've described is how I want it to work.

RockfordLhotka replied on Friday, December 29, 2006

To do a cancel button on the form - for the parent object - does impose some requirements on how you interact with your objects.
 
Most notably - DON'T interact with your objects directly. Instead, always interact with them through the BindingSource objects.
 
Also, make sure to interact with the child bindingsource FIRST, then the parent bindingsource.
 
So a cancel button event handler must go something like this:
 
  childBindingSource.CancelEdit()
  parentBindingSource.CancelEdit()
 
Notice that I'm not interacting with the objects directly - as that confuses the hell out of data binding. And notice that I cancel the child bindingsource first, then the parent bindingsource. This is required because it is the reverse of the way the bindingsource objects did their BeginEdit() calls - so the nesting is unrolled properly.
 
Rocky
 


From: DansDreams [mailto:cslanet@lhotka.net]
Sent: Friday, December 29, 2006 12:49 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] N-Level Undo Trouble - again! - Rocky can you explain?

ajj3085:
DansDreams:
Hit the main cancel button which calls EndEdit on the main/root BindingSource and the first row of the grid does NOT revert back to its original state.


What "main cancel button?"  I don't recall the DGV having a cancel button.  What happens if you just hit ESC?  Or is that what you mean by "main cancel button?"

If you put in a button to call CancelEdit on the collections object, I'd remove it.  The grid should work as expected without you having to put code in to do any of that.

Also, what does GetIdValue return?  If you're using ints, you'll need to keep them unique.

Remember, my scenario is a BusinessBase object containing a BusinessListBase collection of BusinessBase objects:  Account -> OwnerList -> Owner.  My form is for editing the Account and includes a grid for editing the Owners among the controls for editing the Account itself.  So my "main" Cancel button is the one for the whole form - at the Account level.

I'm only editing objects retrieved from the database, so there's no issue with IDs.

I understand part of the problem here may be the way people design their UI.  By that I mean some people may have never thought to have a form like I do.  But because I generally think Microsoft's grids have expected way too much from the user what I've described is how I want it to work.




DansDreams replied on Friday, December 29, 2006

RockfordLhotka:
To do a cancel button on the form - for the parent object - does impose some requirements on how you interact with your objects.
 
Most notably - DON'T interact with your objects directly. Instead, always interact with them through the BindingSource objects.
 
Also, make sure to interact with the child bindingsource FIRST, then the parent bindingsource.

Ok, I think I got it.

This is the code that works:

========

ownersBindingSource.RaiseListChangedEvents = false;
ownersBindingSource.CancelEdit();

accountBindingSource.CancelEdit();

ownersBindingSource.RaiseListChangedEvents = true;
ownersBindingSource.ResetBindings(false);

=======

Apparently, the problem with the simple suggestion to call the child BindingSource first is that MS DataBinding sees that reset of values in the child object as a change and thinks it better call BeginEdit!  Ouch.

At least that what it looks like.  What I saw in the debug messages I was spewing out was that between the time of the start of CancelEdit on the child object and its UndoChanges being completed, a IEditableObject.BeginEdit call was made, which raised the EditLevel artificially so the (rest of the) UndoChanges was just backing out nonsense and ending up right back where it started.

I've tried all kinds of mixes of row navigating and editing and the main Cancel always gets me back to the original state for everything with the code above.  Someone make a note of this for the next time somebody asks.  LOL.

Rocky, here's some more to ponder.  In the case of a form like this, the logic to check if IEditableObject.BeginEdit has already been called and can subsequently be ignored really needs to cascade through the CopyState calls.  That is to say, in this case the initial snapshot throughout the whole object graph as a result of the root object's IEditableObject.BeginEdit call really is what I want considered as the honoring of the first call throughout the whole graph.  I think this also would have solved my problem.  Regarding the previously discussed "switches", perhaps a way to indicate that this "honoring" should cascade as well is another answer.  Then it would indeed make sense to make it a property of Core.BusinessBase where the UI designer would set it according to the design of the particular parent-child UI.

Man, I can't believe I spent all day on THIS!

DansDreams replied on Friday, December 29, 2006

Ah well, I'm not done yet.  Back to the real form with the DevExpress grid I find it still behaves slightly differently - apparently being smarter in that it seems to be holding on to its edit state independent of the BindingSource.  So I think I need to find a way to tell the grid to abandon its edit.

kbcb replied on Friday, July 13, 2007

I'm trying to get this right, right now... Not fully understanding things.   

RockfordLhotka replied on Friday, July 13, 2007

With the DevExpress grid, or the original problem with the child dialog? Very different scenarios...

kbcb replied on Monday, July 16, 2007

We are using the devexpress grids.

Seems as though the Mr. Billings found that devexpress is way different... 'would like to see what he did to solve his problem.

Rocky, do you know the solution to this problem using the devexpress grids?

RockfordLhotka replied on Monday, July 16, 2007

I'm not entirely sure, but this may be one of the scenarios I addressed with some changes in CSLA3 - there was one grid (might be this one) that was serializing each row to implement its own undo, and I had to make a change to BusinessListBase's SetItem code to make the edit levels and object state end up in a consistent state in that case.

alef replied on Friday, August 24, 2007

Hi,

Devexpress or not, the n-level undo is not working the way it should. In my opion the cause is due to databiding. Even in the project tracker example there is a problem with 1-level undo.

So try the following: (this scenario doesn't work)

1) Open a project.

2) Edit the role for the first resource.

3) Click cancel and you see that the modifications aren't rolled back.

 

Try the following (this scenario works)

1) Open a project.

2) Edit the role for the first resource.

3) Click on whatever column on the second resource.

4) Click on Cancel

What I've discovered is that the first item in the resources collection is just by opening the form already on level 2. The root object is still on level 1.  So what I don't understand is that at some way BeginEdit is called on the first object in the child collection without doing this through the root object.

 

 

RockfordLhotka replied on Friday, August 24, 2007

Are you using the 3.0.1 release?

 

There are numerous bugs fixed from 2.1.4 to 3.0.1, both in BusinessListBase, and more importantly in PTWin.

 

Rocky

Copyright (c) Marimer LLC