Strange behavior with BindingSource.CancelEdit()

Posts   
 
    
CurtisB
User
Posts: 6
Joined: 02-Aug-2009
# Posted on: 02-Aug-2009 01:21:02   

2.6 Final DEMO Released May 15th 2009

I have been evaluating LLBlGen Pro over the last week. Up until now everything seems to work as it should and I've been very impressed with the product. I have come across a data binding issue which is either a bug or me not knowing what I'm doing. Below is some sample code to demonstrate the issue. You will need to replace _connectionString and ProductEntity with your connection string and your entity. ProductEntity.Name is a string. Hit the Run Test button and you'll see the following

  1. A change is made to ProductEntity.Name via a bound TextBox and then saved to the db by calling DataAccessAdapter.SaveEntity().
  2. Another change is made to ProductEntity.Name via a bound TextBox and then cancelled by calling _bindingSource.CancelEdit(). ProductEntity.Name is now "" when it should be the value it was after the call to DataAccessAdapter.SaveEntity().

I have managed to work around this problem by using Entity.SaveFields() and Entity.RollbackFields(). This works but I'd prefer not to use this work around. Something like BindingSource.CancelEdit() is basic functionallity that should work.

If I've just got this all wrong I'd be great full to know the correct way to do this.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication
{
    public partial class Form1 : Form
    {
        private Button _test;
        private TextBox _name;
        private BindingSource _bindingSource;
        string _connectionString = "Your connection string here";

        public Form1()
        {
            Size = new Size(350, 120);
            StartPosition = FormStartPosition.CenterScreen;

            _test = new Button();
            _name = new TextBox();
            _bindingSource = new BindingSource();

            Controls.Add(_test);
            _test.Location = new Point(250, 38 );
            _test.Size = new Size(75, 23);
            _test.Text = "Run Test";
            _test.Click += new EventHandler(_test_Click);

            Controls.Add(_name);
            _name.Location = new Point(12, 12);
            _name.Size = new Size(309, 20);
            _name.TabIndex = 2;

            using (DataAccessAdapter adapter = new DataAccessAdapter(_connectionString))
            {
                EntityCollection<ProductEntity> products = new EntityCollection<ProductEntity>();
                adapter.FetchEntityCollection(products, null);

                _bindingSource.DataSource = products;

                Binding binding = new Binding("Text", _bindingSource, "Name");
                binding.DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
                _name.DataBindings.Add(binding);
            }
        }

        void _test_Click(object sender, EventArgs e)
        {
            RunTest();
        }

        void RunTest()
        {
            _name.Text += "_test";
            using (DataAccessAdapter adapter = new DataAccessAdapter(_connectionString))
            {
                ProductEntity product = (ProductEntity)_bindingSource.Current;
                adapter.SaveEntity(product, true);
            }
            MessageBox.Show("Made change to Name and saved to db");

            _name.Text += "_test2";
            _bindingSource.CancelEdit();
            MessageBox.Show("Made change to Name and cancelled");
        }
    }
}
Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 03-Aug-2009 09:05:43   

AFAIK, CancelEdit() will Cancel all editing done to the bound control, whether manually or through databinding, unless and EndEdit() was called, so it will rollback to the value before EndEdit().

Would you please try the following code:

        void RunTest()
        {
            _name.Text += "_test";
            using (DataAccessAdapter adapter = new DataAccessAdapter(_connectionString))
            {
                ProductEntity product = (ProductEntity)_bindingSource.Current;
                adapter.SaveEntity(product, true);
            }
            _bindingSource.EndEdit();
            MessageBox.Show("Made change to Name and saved to db");

            _name.Text += "_test2";
            _bindingSource.CancelEdit();
            MessageBox.Show("Made change to Name and cancelled");
        }
CurtisB
User
Posts: 6
Joined: 02-Aug-2009
# Posted on: 03-Aug-2009 10:47:16   

Walaa, thanks for you response. I've tried what you have suggested and it seems to work. I've done a little more investigation into the IEditableObject interface. Here is what the reference manual says.

BeginEdit - _Begins an edit on an object. _ CancelEdit - _Discards changes since the last BeginEdit call. _ EndEdit - _Pushes changes since the last BeginEdit or IBindingList.AddNew call into the underlying object. _

It seems that EndEdit should be called after DataAccessAdapter.SaveEntity like you suggest. What the documentation doesn't make clear is that EndEdit creates a new save point. CancelEdit should actually say Discards changes since the last BeginEdit or EndEdit call.

It seems that there is a bug here even though I didn't call EndEdit the BindingSource called BeginEdit when the BindingSource.DataSource was set to my EntityCollection. When I called CancelEdit the entity should have been rolled back to its original state. It seems that DataAccessAdapter.SaveEntity discards the values saved when BeginEdit is called.

I did some testing by overriding OnBeginEdit and OnEndEdit in my entity class. It seems that the BindingSource calls these methods when BindingSource.Position changes. So just moving to the next entity would also solve this problem as well.

Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 03-Aug-2009 13:48:02   

When I called CancelEdit the entity should have been rolled back to its original state. It seems that DataAccessAdapter.SaveEntity discards the values saved when BeginEdit is called.

Are you speaking about the entity or the bound control?

CurtisB
User
Posts: 6
Joined: 02-Aug-2009
# Posted on: 03-Aug-2009 22:10:46   

Are you speaking about the entity or the bound control?

Both, this issue it not really about data binding its really about IEditableObject. The BindingSource just happens to know about IEditableObject and what methods should be called at specific times. Because the TextBox is bound to the Name property of the entity it's Text value will reflect the value of that property.

BindingSource.CancelEdit will call Entity.CancelEdit. This should rollback the entity to the state that was saved when Entity.BeginEdit or Entity.EndEdit was last called. In my test code Entity.Name was rolled back to string.Empty. When Entity.BeginEdit was called by the BindingSource Entity.Name was not string.Empty. I think this is not the correct behavior for IEditableObject.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 04-Aug-2009 07:06:08   

In my opinion: the Entity's EndEdit is (and should be) used by the bindingSource, when it ends editing. From the Entity's point of view, the changes were persisted to DB. From the Binding's point of view, the properties changed but you are not done editing.

So, I think this should be viewed from you GUI's point of view. What are you doing in your GUI? When is supposed the GUI accept changes?, etc.

David Elizondo | LLBLGen Support Team
CurtisB
User
Posts: 6
Joined: 02-Aug-2009
# Posted on: 04-Aug-2009 08:57:35   

Surely editing ends when I decide it should end. This decision is not up to the BindingSource to decide. If this was the case then BindingSource.EndEdit should not be public. In my case I have a Save and a Cancel button. These buttons are initially disabled. When editing begins these buttons are enabled. When the Save button is hit I call BindingSource.EndEdit. This is because the user has decided that editing is complete. Changes are then saved to the db and the buttons are disabled.

Like I said before this issue is not really about data binding. It's about IEditableObject and entities not behaving correctly when BindingSource.CancelEdit is called after DataAccessAdapter.SaveEntity. If DataAccessAdapter.SaveEntity is not called Entity.CancelEdit works as it should. DataAccessAdapter.SaveEntity must be breaking the restore state logic. This is not a big deal because as Waala pointed out DataAccessAdapter.EndEdit needs to be called after DataAccessAdapter.SaveEntity and this seems to fix the problem.

This is a small issue that is not going to cause me a problem. Based on the testing and evaluation of llblgen I've done over the last week I'm going to purchase the product. I've looked around at other products over the last month and can say that llblgen in my opinion is light years ahead of it's competitors. I've looked at Linq to Sql, Linq to Entities, Nhibernate, Dev Express ORM and others.

When will v3 be available?

Walaa avatar
Walaa
Support Team
Posts: 14995
Joined: 21-Aug-2005
# Posted on: 04-Aug-2009 09:50:34   

This is a small issue that is not going to cause me a problem.

We'll further investigate it.

Based on the testing and evaluation of llblgen I've done over the last week I'm going to purchase the product. I've looked around at other products over the last month and can say that llblgen in my opinion is light years ahead of it's competitors. I've looked at Linq to Sql, Linq to Entities, Nhibernate, Dev Express ORM and others.

Thank you very much for the positive feedback.

When will v3 be available?

The target is to release it by end 2009. Here are some good info about v3: http://www.llblgen.com/TinyForum/Messages.aspx?ThreadID=14722

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39897
Joined: 17-Aug-2003
# Posted on: 04-Aug-2009 17:37:54   

It indeed seems a bit odd what happens: saving the entity should simply mean: things are persisted, this is reality from now on. But the entity is still inside the middle of an edit cycle (something which is called 'complex databinding', which uses IEditableObject, implemented on the entity and fields). Cancelling the edit, rolls back the edit that was in progress (saved or not), so it rolls back to the state it had when the edit started.

What you saw was that it rolled back to string.Empty and indeed it should roll back to the value it had stored inside the entity. The cause of that is the save action: it applies the changes, without checking whether it's inside an edit cycle. Changing that behavior will break code so we won't make that change at the moment, as the behavioral change is in the middle of a release (not a major version change). I also have doubts it would be the logical thing to do (see below)

What you could do is when you click save, call 'EndEdit' on the datasource as suggested above, which calls endedit on the entities, and then proceed with the save action.

Rolling back to a previous value/values after that by clicking cancel in fact requires you to roll back the values inside the database as well (as you persisted values!). So you then either have to roll back to a previous set of values and persist these as well, or setup your UI a bit different: when the user clicks Save, this dialog closes so cancel isn't available anymore (as the user persisted the changes) and cancel rolls back to a previous state (i.e. the state when the dialog was opened). SaveFields/RollbackFields are meant for that scenario actually. You can also decide to fetch data for the dialog, and if the user clicks Cancel, simply throw the graph away.

Does that work for you? Or did I miss something (could be, these kind of things are often cumbersome to solve... databinding is nice but can be tricky as well) ?

Frans Bouma | Lead developer LLBLGen Pro
CurtisB
User
Posts: 6
Joined: 02-Aug-2009
# Posted on: 04-Aug-2009 22:57:12   

Thanks for your input Otis. I think that you haved missed something here. I'm not trying to rollback changes that have been persisted to the db. The sequence is as follows...

  • Fetch entity from db.
  • The user makes some changes to the entity using the UI.
  • The user hits the Save button and we call DataAccessAdapter.SaveEntity.
  • The user then decides to make more changes to the same entity.
  • The user then changes thier mind and hits Cancel.
  • We call BindingSource.CancelEdit.
  • The values that are then rolled back are now string.Emtpy or whatever the default for that property type is.

Any call to BindingSource.CancelEdit doesn't work after DataAccessAdapter.SaveEntity has been called on that entity unless BindingSource.EndEdit is called first. The BindingSource only calls EndEdit when BindingSource .Position is changed so if a user decides to make more changes to the same entity after a save without moving to another enity first it won't work if they decide to hit Cancel.

Walaa is correct in saying that after calling DataAccessAdapter.SaveEntity BindingSource.EndEdit should be called to tell the IEditableOject that editing has completed and to create a new save point. The bug here is that when BindingSource.BeginEdit is called when editing begins this should create a new save point and it does but when DataAccessAdapter.SaveEntity is called this save point seems to be trashed.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39897
Joined: 17-Aug-2003
# Posted on: 05-Aug-2009 09:23:37   

You assume that a new BeginEdit() occurs when you edit the textbox again after the save, but I'm not so sure that is what happens (i.e.: as the currentposition doesn't change, no BeginEdit is called, and therefore, no new value is set as the startvalue, and thus canceledit rolls back to the assumed previous value which was reset after the save).

I'll check if what I assumed is indeed what happens (as that's the only logical explanation I can think of)

(edit) I can reproduce your behavior. Looking into it.

(edit) The problem is this: - when the collection is bound to the bindingsource, BeginEdit is called on the entity instances and one time again on the actual entity (all done by the bindingsource). The BeginEdit on the entity calls the beginEdit on the fields if an edit cycle isn't already in progress (as BeginEdit is called lot more times than you think and at least more times than necessary, as you might expect that BeginEdit is called just once and only on the entity which is at the actual postion... . No call to EndEdit or CancelEdit at this point...

So far so good. You make an edit through code. Remember: endedit hasn't been called! This triggers again a call to BeginEdit. As a cycle is already in progress, nothing happens. The change is made. Then you save the entity, acceptchanges is ran on the saved entity's fields, clearing the _originalValue member.

Then you make another change, again BeginEdit is called, but as a cycle is still in progress (no EndEdit/CancelEdit has been called) it doesn't start a new cycle. Then you call CancelEdit and this calls CancelEdit on the entity and the fields, but as _originalValue has been cleared by the save, it reverts to the default value: null.

The main problem IMHO is that EndEdit is never called by the bindingsource. This leads to the calls to BeginEdit multiple times which have no effect as a cycle is already in progress. The flag to check whether a cycle is in progress is necessary because otherwise the 'originalvalue' is overwritten multiple times when the user makes subsequential changes or at other times when the user changes rows in a grid for example. (also, when an entity is bound to multiple controls, it needs this flag)

What are the remedies against this? Either that (a) the Save action on an entity resets the flag if an edit cycle is in progress or (b) the save action doesn't call ApplyChanges() when there's an edit cycle in progress.

(a) has the downside that if the collection is bound to a control directly, it depends on the control if it calls BeginEdit every single time the user makes a change (and this isn't the case with all grids for example). (b) has the downside that it leaves _originalValue as-is and there might not be another edit so this value is kept in memory while there's no cycle in progress.

As this code is a pain to get right because of the obscurity of the mechanism of IEditableObject (for example, a DataTable also requires EndEdit in this scenario), we decided not to make any change.

Frans Bouma | Lead developer LLBLGen Pro
CurtisB
User
Posts: 6
Joined: 02-Aug-2009
# Posted on: 05-Aug-2009 09:55:58   

Otis I see your point, and I know that BeginEdit is not called again. So I guess I need to go back on what I said before about rolling back changes that had already been saved to the db. My example was a bad one. However my point here is that at no stage was the Name property of my entity equal to string.Empty so rolling back changes should not set the Name property to string.Empty. The fact that I hadn't called BeginEdit or EndEdit is beside the point.

Anyway I don't want to go on about a small issue when it's plain to me that this is a very powerful product. We actually started project a couple of months ago and were using Linq to SQL which worked quite well initially. Then we hit some road blocks and decided to go looking for an alternative. We have found it in llblgen.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39897
Joined: 17-Aug-2003
# Posted on: 05-Aug-2009 10:28:42   

Thanks Curtis simple_smile

I updated my previous post with more explanations about what happens. We don't fix this at this point because of the uncertainty that it will not break any applications. We now are aware of this issue thanks to you, so next time when someone reports this we at least know the reason why this happens. I'll add a change request for v3 for this, so we'll look into it when we're fixing the open issues which are postponed till v3 due to the chance they might break code.

The empty string is caused by the clearning of the originalvalue member after the save so it's actually reverting to null (and the property returns "" as default value for a nulled string).

Frans Bouma | Lead developer LLBLGen Pro