There's been some discussion around some issues with n-level undo and data binding - specifically some nasty interactions between the BindingSource controls and the IEditableObject interface implemented by BusinessBase.
(this is the source of at least one thread in the old forum, and at least two email discussions I've been having with readers)
The primary symptom is that some object ends up with its edit level at 1 even though, by all understanding, all objects should have edit level of 0.
The most common scenario is where a parent object, with a collection of child objects, is bound to controls on a single form (detail controls for the parent, grid for the child collection). In this scenario, one of the child objects ends up at edit level 1, even though the parent and all other child objects edit levels are at 0 because of an ApplyEdit call.
After researching this, I think I have the answer. This is not due to a bug in CSLA, or even misbehavior by either data binding or CSLA. Rather it is a lack of complete understanding (at least on my part) as to how to properly use the BindingSource controls (or at least that's how I'm viewing this at the moment )
When you set up data binding in your form's constructor or load event handler, you may or may not manually call BeginEdit on the parent object (let's call it Employee). Then you set the BindingSource control's DataSource property:
Dim emp As Employee = Employee.GetEmployee(42)
emp.BeginEdit() ' optional
Me.EmployeeBindingSource.DataSource = emp
This not only starts data binding on the parent, but on its list of child objects (let's say phone numbers). Data binding, at this point, uses IEditableObject to call BeginEdit on the parent object - which is why the explicit BeginEdit call is optional - that call will happen one way or the other, but CSLA automatically makes sure only one call is actually honored.
Later, behind your Save button things get complex. In the book what I did (which is wrong) is to call ApplyEdit and Save. This, as it turns out, doesn't work right. Instead, you need to call EndEdit() on the BindingSource controls (not just the parent one, but the child one too!!) like this:
Me.EmployeeBindingSource.EndEdit()
Me.PhoneListBindingSource.EndEdit()
The first line automatically calls ApplyEdit (actually IEditableObject.EndEdit) on the parent object, which cases a cascade call down to all child objects. This puts all objects at edit level 0 EXCEPT for the one row in the grid that is currently being edited by the user.
The second line causes an ApplyEdit (actually IEditableObject.EndEdit) call to the currently selected child object in the child collection. The result is that its edit level is now also set to 0.
With that done, you can proceed to save your object - using or not using the Clone() technique as discussed in the book, etc.
I think this resolves the issues people have been having. Certainly it appears to resolve them in my testing around this problem. Once I have independant confirmation (which can come from any/all of you who care to test the solution) I'll put entries on www.lhotka.net as errata.
I am seeing what I assume to be another aspect of this problem, which doesn't involve the save button.
To use your example I have an employee with it's grid of phone numbers. I click on a grid row to open up a modal dialogue box for one of the phone numbers. I do a beginedit in the dialogue box because it would be confusing if a undo action inside the dialogue box was able to undo an uncommitted change which was made back in the mainform. The closing event of my dialogue box has a canceledit.
Use of the dialogue box elevates the editlevel of the examined phone number with relation to unexamined phone numbers (presumably due to the implicit beginedit mentined in your post, and Ross' (Tetranz4) post in the old newsgroup.
This means that a canceledit back in the mainform can cause corruption, as any rows examined by the dialogue box are not undone.
The issue can be resolved by calling canceledit twice on the phone number in the closing event of the dialogue box. Once for my BeginEdit and once for the implicit BeginEdit caused by the attachment of the datasource.
However, unless I have a bug of my own this observation seems to call into question whether, in this case, you are correct when you say "but CSLA automatically makes sure only one call is actually honored." ?
CSLA automatically makes sure one call through IEditableObject is honored. It honors all explicit calls to BeginEdit, etc.
But I think your issue comes from the same source. The current row in the grid has its edit level elevated. Then you open your modal dialog, and so that child object's edit level isn't where it should be. I think that before opening the dialog you need to do an EndEdit on the BindingSource for the grid, thus lowering the edit level of the child object to match that of its parent and siblings.
This way the modal dialog can do its own BeginEdit, and Cancel/ApplyEdit (or use the BindingSource to do it).
Thanks, that works.
I was getting confused thinking that the EndEdit would commit the change, but of course it won't because the row is a child.
This is a tricky issue isn't it? We are having to explicitly back out the change to an invisible thing (editlevel) which was called by an implict call that we didn't make. This new .NET data binding support is helping productivity, but has it's traps for young players.
Hi
I doesn't make the test with CSLA 2.0 but with the version 1.5 each time you move from one row to another in a readonly grid, BeginEdit is called causing serialization of the data bound business object.
The ApplyEdit /EndEdit/cancelEdit is never called because we don't make any modification operation on the row item. The edit level is incremented by one each time you select another row.
This has big resource and performance impact.
In CSLA 1.5, I made a patch for temporally disable beginedit(serialization) for business object bound to a readonly grid.
Do you think the problem still existing in version 2.0?
Is there another way to avoid BeginEdit execution on readonly grid?
Thanks
Alain
I was one of the people who had an issue with this, but have not had a chance to do any testing yet. However, I will point out that I feel it is very confusing for a UI developer to have to call BeginEdit on the Object but then EndEdit on the BindingSource, especially when they have to do so for both the parent and child. I get the feeling I will still have to leave the IEditableObject stuff out which is a shame as I do not want to change the Csla unless absolutely necessary.
John.
1) There is no way I know of to prevent Windows Forms from calling BeginEdit. And if you defeat that call, then the user couldn't press ESC in a grid and have the cancel operation work. In other words, BeginEdit is actually required if you are doing in-place editing in a grid, or the user won't get the expected experience.
2) You do not have to manually call BeginEdit in .NET 2.0. I suggested doing that in the book, and it doesn't hurt anything - but in researching this issue it has become clear that the new BindingSource controls are very aggressive in calling BeginEdit (they do it immediately) and so there is no reason to call BeginEdit manually.
In .NET 1.x calling BeginEdit manually was wise because it avoided some edge case issues where edits could happen before BeginEdit was called automatically. But in .NET 2.0 this no longer appears to be the case and so I don't think calling it manually is important any more. Hopefully this addresses the UI developer confusion worry.
I'm hesitant to resurrect an older thread, but I'm having a problem with a situation almost like the one described here.
I have a Products object (type BusinessListBase) which contains Product objects (BusinessBase). The Products object is bound to a Binding Source, which is used by a grid. Users can select a Product from the grid to display information in databound controls on the same form. That part is working, as well as the code to save the Products back to the database.
Here's where it goes awry: The users can also select a Product and choose to edit it on another form. That form has a Binding Source, to which I pass the Product in question. They can use it to capture changes, and when closing the form have the choice to keep them (bsProduct.EndEdit) or discard them (bsProduct.CancelEdit). The first time the users open the form, capture changes, and save them, the changes are correctly saved. But second and subsequent times the form is used (and they are that indecisive, I've watched them do it), the changes do not make it, even though Product.IsDirty, IsValid (and therefore IsSavable, IIRC) are all True.
I've tried every fix proposed in this thread, to no avail. (but I've also put in almost 120 hours at work in the last 7 calendar days, so I'm not feeling very smart) Please tell me what I'm missing.
EndEdit yes, Modal no (I'll try that). If they want to continue editing the object on the main form, do I have to call any methods / reset any bindings?
From: Norma_B [mailto:cslanet@lhotka.net]
Sent: Friday, June 30, 2006 9:25 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] n-level undo and Windows Forms data binding
Thanks for your time, I'll make the detail forms modal and see how that works.
From: Norma_B [mailto:cslanet@lhotka.net]
Sent: Friday, June 30, 2006 10:39 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: n-level undo and Windows Forms data binding
RockfordLhotka:There's been some discussion around some issues with n-level undo and data binding - specifically some nasty interactions between the BindingSource controls and the IEditableObject interface implemented by BusinessBase.
(this is the source of at least one thread in the old forum, and at least two email discussions I've been having with readers)
The primary symptom is that some object ends up with its edit level at 1 even though, by all understanding, all objects should have edit level of 0.
[Snip ...]
Thanks Rocky, that seems to work. My cancel button now calls CancelEdit on the collection bindingSource and then CancelEdit on the root bindingSource.RockfordLhotka:But the end result of this was that you should only interact with the bindingsource object in terms of begin/cancel/end edit calls.
Hi,
Having made some tests on ProjectTracker, i discovered that when CopyState method
gets called on a collection like projects, for example after a manual or a framework-call BeginEdit, _bindingEdit is not set to true on the projectresources child objects as i would have been expected. I'm asking if is this the expected behaviour. If not so, is it correct overriding the CopyStateComplete method on BusinessBase class end setting _bindingEdit to true, there?
I'm just a CSLA .NET newbie, so forgive me for the silly question, and for my english!
Thanks,
Nando
From: derfy [mailto:cslanet@lhotka.net]
Sent: Sunday, July 30, 2006 11:32 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] n-level undo and Windows Forms data bindingHi,
Having made some tests on ProjecTracker, i discovered that when CopyState method
gets called on a collection like projects, for example after a manual or a framework-call BeginEdit, _bindingEdit is not set to true on the projectresources child objects as i would have been expected. I'm asking if is this the expected behaviour. If not so, is it correct overriding the CopyStateComplete method on BusinessBase class end setting _bindingEdit to true, there?I'm just a CLSA newcomer, so forgive me for the silly question, and for my english!
Thanks.
Hi Rocky,
What i missed before, was why the _bindingEdit member of the _project instance is set to true at the
_project.BeginEdit() call in the ResourceEdit constructor, but not the _bindingEdit member of its
ProjectResources children objects. Your timely reply helped me to further clarify the role of the _bindingEdit member.
Thanks again for your patience.
Nando.
Jimbo:In the UI (form) constructor, BeginEdit is set on the collection object. (standard stuff to initiate csla undo)
Jimbo:As at no point did we manually or programatically call Begin, Apply or CancelEdit. Meaning, to most peoples that the edit level should still be at the point that we initiated in the constructor and that all action since would be undone.
Jimbo:But this is not the case ... Please explain how we deal with this and achieve the user experience that is expected.
I was at a conference last week, and am at a different one this
week (and unfortunately got the flu too – bad enough I spent half of
today in the hospital L )
While flying out to the conferences I did a little testing on
this, and my preliminary findings are that the DataGridView calls EndEdit “on
your behalf” when the grid loses focus. Of course it loses focus when you
click the Cancel button…
If your cancel button is on a toolstrip though, then it doesn’t
get focus and the issue doesn’t appear to occur.
I didn’t post this earlier, because I didn’t get a
lot of time to fully explore this effect, but given my past few days, it is
clear I won’t get to it until late next week at the earliest. Jimbo, you
may want to check on this focus/endedit issue and see if putting your cancel
button in a toolstrip solves your issue.
Rocky
From: ajj3085
[mailto:cslanet@lhotka.net]
Sent: Tuesday, March 27, 2007 1:28 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: n-level undo and Windows Forms data binding
Jimbo,
You're right; there's no BeginEdit on the BS, but I think it will call that
automatically for you (there is an EndEdit). As far as going against the
book, if you search the forum you'll see that at the time of the writing, Rocky
wasn't aware of the problems of not keeping the BS 'in the loop.' He now
recommends you interact only with the binding source.
Now, when you say its unreliable when applying changes programatically.. how
are you doing so? Are you calling EndEdit on the binding source? If
not, you'll run into similar problems.
To successfully use the n-level undo, I think you'll have to keep a 'hands off'
approach until you actually want to cancel or accept all changes on the
collection... and then you should only do so through the binding source.
Data binding should be calling BeginEdit for you automatically.
To do my test I just overrode the AcceptChangesComplete and CancelChangesComplete so I could put breakpoints and see the call stack.
Interestingly enough – and this is why I was hesitant to talk about this – my Cancel button code DID NOT RUN after that automatic EndEdit. I can’t believe that’s actually the case, so I assume I had a bug in my test …
I have also problems with DataGridView and undo. I think there is a bug somewhere . How to reproduce it :
1.Open “ProjectTracker20cs”
2.Run PTWin
3. login as pm
4. Edit “Expert One-on-One VB Business Objects “
5. Change Role in all records
6. Set focus on one record in the grid
7. Press Cancel
You will se that only 2 rows did undo operation . One row was unchanged .
Look at this :
private void Cancel_Button_Click(object sender, EventArgs e)
{
this.projectBindingSource.CancelEdit();
this.resourcesBindingSource.CancelEdit();
}
I transformed it to
private void Cancel_Button_Click(object sender, EventArgs e)
{
this.projectBindingSource.DataSource = typeof(ProjectTracker.Library.Project); ;
Project.CancelEdit();
this.projectBindingSource.DataSource = Project;
}
And noticed that line:
this.projectBindingSource.DataSource = Project;
calls BeginEdit() twice .
one at:
project.BeginEdit();
second at:
project.resources[0].BeginEdit();
this result in situation like this :
name EditLevel
project 1
resource1 2
resource2 1
resource3 1
When we fire undo operation it lower our level by one
name EditLevel
project 0
resource1 1
resource2 0
resource3 0
and again there are two calls from binding sources:
First:
name EditLevel
project 1
resource1 2
resource2 1
resource3 1
Second:
name EditLevel
project 1
resource1 3
resource2 1
resource3 1
I think that Parent editLevel and collection editLevel should be >= resource editlevel . I tried this :
this.ResourcesDataGridView.DataSource = typeof(ProjectTracker.Library.ProjectResource);
((ProjectTracker.Library.ProjectResource)this.resourcesBindingSource.Current).CancelEdit();
((ProjectTracker.Library.ProjectResource)this.resourcesBindingSource.Current).CancelEdit();
this.projectBindingSource.CancelEdit();
this.resourcesBindingSource.CancelEdit();
this.ResourcesDataGridView.DataSource = resourcesBindingSource;
On each call:
((ProjectTracker.Library.ProjectResource)this.resourcesBindingSource.Current).CancelEdit();
i got BeginEdit() on thesame object so this line does nothing :/
I dont have a clue how to solove this :( .
I just discovered this too - the issue (I think) is that there's a bug in the cancel button handler. Notice that the parent bindingsource is canceled before the child - that's wrong. It should be
this.resourcesBindingSource.CancelEdit();
this.projectBindingSource.CancelEdit();
It turns out that the real solution is a bit more complex than what's been discussed here. In CSLA .NET 3.0 I added code so objects will refuse to apply/cancel their changes if there's a mismatch between the parent and child edit levels. This revealed some lurking issues with Windows Forms data binding and cancel buttons.
As a result, the new ProjectTracker's PTWin app has corrected code as described in this article.
RockfordLhotka:_bindingEdit should only be set to true in the case that BeginEdit was called through the IEditableObject interface.CSLA .NET supports n-level undo. IEditableObject only supports 1 level undo. The _bindingEdit flag exists to allow IEditableObject to do its 1 level of undo by using the pre-existing n-level undo support in CSLA.I discuss this quite a bit in Chapter 3 in the book, I recommend reading through that to get a better understanding of the design tradeoffs involved here.Rocky
From: derfy [mailto:cslanet@lhotka.net]
Sent: Sunday, July 30, 2006 11:32 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] n-level undo and Windows Forms data bindingHi,
Having made some tests on ProjecTracker, i discovered that when CopyState method
gets called on a collection like projects, for example after a manual or a framework-call BeginEdit, _bindingEdit is not set to true on the projectresources child objects as i would have been expected. I'm asking if is this the expected behaviour. If not so, is it correct overriding the CopyStateComplete method on BusinessBase class end setting _bindingEdit to true, there?I'm just a CLSA newcomer, so forgive me for the silly question, and for my english!
Thanks.
Copyright (c) Marimer LLC